Design Documentation - snuhcs-course/swpp-2026-project-team-14 GitHub Wiki

Design Documentation

Document Revision History

Version Date Author Changes
0.1 2026-10-06 Dongkun Moon Created the page: overall system architecture, and the body analysis design (P9, P10): architecture, external libraries, decisions, API, data structures and algorithms.
0.1 2026-10-06 KiHwan Kim Added contents about the GitOps deployment infrastructure: GitHub Actions, GHCR, Argo CD, k3s, and namespace configuration (P6, P7, P15).
0.2 2026-10-07 Hyeon U Jeong Added the wardrobe architecture, API contract, data model and measurement pipeline; moved Design Patterns to section 4.
0.3 2026-10-16 Hyeon U Jeong Added login/onboarding architecture, authentication/session APIs, User/token/limit models, account ownership, local storage and manual imports; aligned existing storage/API descriptions with PR #26 and moved Design Patterns to section 5.

1. System Architecture

1.1 Overall

StyleMate is a client–server system: one Android app and one Django backend, deployed with a GitOps pipeline. Each feature is a separate Django app with its own API under /api/, and a separate screen package in the Android app.

flowchart LR
  subgraph Phone["Android app (Kotlin, Jetpack Compose)"]
    BP["Body profile<br/>onboarding · results · My Profile"]
    WR["Wardrobe<br/>list · AR capture · garment editor"]
    PL["Home · outfit recommendation ·<br/>conversational editing · products<br/>(planned)"]
  end

  subgraph Backend["Django backend (one service)"]
    BA["apps/body_profiles<br/>POST /api/body-profile/analyze/"]
    WA["apps/wardrobe<br/>analysis draft · save · edit · list"]
    RA["recommendation / chat apps<br/>(planned)"]
  end

  subgraph AI["AI models and services"]
    MP["MediaPipe Pose<br/>(on server)"]
    HR["HRNet garment landmarks<br/>(ONNX, on server)"]
    GM["Gemini API<br/>(name, category, color)"]
  end

  DB[("MySQL<br/>garments, measurements, memos")]
  FS[("Server file storage<br/>garment photos")]

  BP -- "HTTPS (REST, multipart)" --> BA
  WR -- "HTTPS (REST)" --> WA
  PL -.-> RA
  BA --> MP
  WA --> HR
  WA --> GM
  WA --> DB
  WA --> FS
Loading
Component Status (Iteration 1) Notes
Body profile (app + apps/body_profiles + body_analysis) implemented Stateless: body photos are analysed in memory and never stored. Details in §1.2–§2.
Wardrobe (app + apps/wardrobe) implemented AR capture with ARCore. HRNet proposes garment points, the app computes cm on the floor plane. Gemini suggests name, category and color; the user edits and saves. Details in docs/wardrobe/.
MySQL implemented for the wardrobe Garment attributes, measurements, memos and photo paths.
Image storage server file storage S3-compatible object storage is planned.
Outfit recommendation, conversational editing, product recommendation planned Will read body measurements and wardrobe items through the APIs above.

Login/onboarding integration (PR #26): app-specific ID/password authentication, optional eight-step onboarding, style-profile editing and explicit guest imports are implemented. MySQL now also stores User, AuthToken and AuthRateLimit records and account-owned garments/outfits. Guest data stays in app-private device storage. See §1.6 and §4.

Deployment:
The current repository defines one GitOps deployment path for the backend:

main branch
→ GitHub Actions backend release workflow
→ GHCR backend image
→ Argo CD Application
→ k3s workload in team14 namespace
Layer Current configuration
CI Backend and Android CI workflows run from .github/workflows/.
Release .github/workflows/backend-release.yaml releases backend changes from main.
Image registry The backend Docker image is published to GitHub Container Registry (GHCR).
GitOps controller Argo CD runs in the argocd namespace.
Argo CD Application infra/argocd/team14.yaml tracks main and reads manifests from infra/apps.
Application namespace Argo CD deploys the application resources to the team14 namespace.
Backend workload infra/apps/backend.yaml defines the backend Deployment and ClusterIP Service on port 8000.
Runtime k3s runs on the Azure VM (2 vCPU, 8 GiB).

Argo CD automatically reconciles the desired state in Git with the k3s cluster. The current codebase has no separate Argo CD Application or namespace for develop; only main → team14 is configured.

Team-level decisions:

Decision Rationale
Native Android (Kotlin + Compose) instead of a responsive web app Camera, AR (ARCore) and gallery access, and testing on the target devices (Galaxy S23).
One Django backend, one app per feature Shared deployment and configuration; features can be developed and tested independently.
Models run on the server, external LLM only where needed Pose and garment-landmark models are bundled in the image (no per-request cost). Gemini is used for open-ended description of garments.
GitOps (GHCR + Argo CD) Every merge to main is deployed the same reproducible way; the cluster state is versioned in infra/.

1.2 Body analysis (owner: Dongkun Moon)

A client–server design. The Android app uses MVVM: Compose screens observe a ViewModel, which calls a BodyAnalyzer interface. The server is a Django endpoint that delegates to a framework-independent Python package, body_analysis.

flowchart LR
  subgraph App["Android app (Kotlin, Jetpack Compose)"]
    UI["Screens<br/>CaptureGuide · PhotoInput · Analyzing<br/>Review · MyProfile · BodyFigure"]
    VM["BodyProfileViewModel<br/>SetupState / AppState (StateFlow)"]
    IF[["BodyAnalyzer (interface)"]]
    RA["RemoteBodyAnalyzer<br/>(OkHttp)"]
    FA["FakeBodyAnalyzer<br/>(previews, tests)"]
    UI -- "user events" --> VM
    VM -- "state" --> UI
    VM --> IF
    IF -.-> RA
    IF -.-> FA
  end

  subgraph Server["Django backend"]
    V["apps/body_profiles<br/>views.analyze: validation, size limit"]
    S["services: one pipeline per process,<br/>lock around analysis"]
    V --> S
  end

  subgraph Pkg["body_analysis package (pure Python)"]
    D["decode_image<br/>(OpenCV, ≤ 1280 px)"]
    R["ReferenceSet<br/>benchmark marker"]
    P["MediaPipePoseEstimator<br/>landmarks + person mask"]
    Q["check_quality<br/>retake hints"]
    M["measure<br/>widths × depths → cm"]
    C["ClothingDetector<br/>loose top / bottom"]
    K["MeasurementCorrector<br/>learned ridge correction"]
    D --> R --> P --> Q --> M --> C --> K
  end

  RA -- "HTTPS multipart POST<br/>front, side, height, weight?, gender?" --> V
  S --> D
  K -- "measurements, warnings,<br/>clothing, reference (JSON)" --> RA
Loading

Deployment (team infrastructure): GitHub Actions builds the backend Docker image and pushes it to GHCR. Argo CD deploys it to k3s on an Azure VM (2 vCPU, 8 GiB). The pose model is downloaded into the image at build time and checked against a checksum.

1.3 External libraries (body analysis)

Library Where Used for
Jetpack Compose, Material 3 app UI, including the Canvas-drawn body figure
AndroidX Lifecycle (ViewModel), Kotlin Coroutines / StateFlow app MVVM state, background work
OkHttp 4.12 app multipart upload to the API
AndroidX ExifInterface app rotating camera photos upright before upload
Django 5.2, gunicorn server REST endpoint, production server
MediaPipe 1.0 — Pose Landmarker (heavy) server 33 body landmarks and a person segmentation mask per photo
OpenCV, NumPy server image decoding and resizing, mask geometry, model inference
Anny (NAVER, Apache 2.0), trimesh, PyTorch, scikit-learn offline only synthetic 3D bodies and their true measurements, model training (not in the server image)

1.4 Architectural decisions

Decision Rationale
Analysis on the server, not on the phone One Python pipeline that we can benchmark and improve without app releases. The heavy pose model and learned models stay off the phone. On-device pose extraction remains an option if privacy rules require it.
body_analysis is a pure Python package with no Django imports The same code runs in the API, the benchmark and training scripts, and tests. Each stage sits behind a small interface (PoseEstimator), so it can be replaced or faked.
Photos are never stored They may show underwear. Uploads are read into memory, analysed and dropped. There is no image storage or third-party API on this path, which also removes a dependency on the storage service.
REST with a single multipart request Analysis is one request and one response (≈ 0.5 s); there is nothing to stream, so no WebSocket. Multipart sends both photos and the form fields together.
Numbers are the contract Other features (recommendation, conversational editing) read measurements and derived ratios, never photos.
The client depends on the BodyAnalyzer interface The UI was built against FakeBodyAnalyzer before the server existed, and switching to RemoteBodyAnalyzer needed no UI changes. Tests use the fake or a mock server.
Both front and side photos are required Circumferences need the body's depth, which only the side photo shows.
Clothing is detected, not asked Users answered the clothing question carelessly, and one answer cannot say which garment is loose. A detector per region (top / bottom) is more reliable.

1.5 Wardrobe — Hyeon U Jeong

The wardrobe uses the shared Android app and Django backend. Compose screens and ViewModels manage browsing, analysis and editing. A separate capture activity manages the AR session and frozen camera frame.

%%{init: {"theme":"base","flowchart":{"curve":"stepBefore","nodeSpacing":36,"rankSpacing":55,"htmlLabels":true},"themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","clusterBkg":"#000000","clusterBorder":"#FFFFFF","edgeLabelBackground":"#000000","fontFamily":"Arial","fontSize":"16px"}}}%%
flowchart LR
  subgraph App["Android app (Kotlin, Jetpack Compose)"]
    UI["Wardrobe screens + ViewModels<br/>list · analysis · editor"]
    CAP["MeasurementActivity<br/>MeasurementCamera · ARCore"]
    GEO["MeasurementGeometry<br/>plane-based cm calculation"]
    NET["Wardrobe API clients<br/>HttpURLConnection"]

    CAP -->|"calculate"| GEO
    UI --> NET
    CAP -->|"detect points"| NET
  end

  subgraph Server["Django backend"]
    API["Wardrobe API<br/>apps/wardrobe"]
  end

  subgraph AI["Models and services"]
    HR["GarmentIQ HRNet<br/>ONNX Runtime · on backend"]
    GM["Gemini API<br/>name · category · colors"]
  end

  subgraph Storage["Storage"]
    DB[("MySQL<br/>garment records")]
    FS[("File storage<br/>garment photos")]
  end

  NET -->|"REST"| API
  API -->|"detect landmarks"| HR
  API -->|"analyze photo"| GM
  API -->|"read / write"| DB
  API -->|"read / write"| FS

  classDef default fill:#333333,stroke:#FFFFFF,color:#FFFFFF
  style App fill:#000000,stroke:#FFFFFF,color:#FFFFFF
  style Server fill:#000000,stroke:#FFFFFF,color:#FFFFFF
  style AI fill:#000000,stroke:#FFFFFF,color:#FFFFFF
  style Storage fill:#000000,stroke:#FFFFFF,color:#FFFFFF
  linkStyle default stroke:#FFFFFF,color:#FFFFFF
Loading

Diagram image

External libraries and services

Library / service Where Used for
Jetpack Compose, Material 3, Lifecycle, Kotlin Coroutines Android Shared UI, state and asynchronous requests
Google ARCore Android Camera tracking and a metric floor plane
Android HttpURLConnection Android Image uploads and wardrobe REST requests
Django, mysqlclient Backend API handling, validation and MySQL persistence
Pillow, NumPy, ONNX Runtime Backend Image preparation, coordinate processing and CPU inference
GarmentIQ HRNet, pretrained on DeepFashion2 Backend Garment landmark heatmaps
Gemini gemini-3.1-flash-lite External service Suggested name, category and representative colors
PyTorch and ONNX Docker build only Exporting pretrained weights to the runtime model

Architectural decisions

Decision Rationale
AR geometry on the phone; landmark detection on the server ARCore provides the camera pose and metric plane for the captured frame. The server runs the existing garment model without requiring its weights on the phone.
Separate measurement from description HRNet locates image points, ARCore supplies metric geometry, and Gemini describes visible attributes. Gemini is not asked to invent measurements or material properties.
REST requests for analysis and point detection Each request has a bounded input and response. One in-flight preview request avoids an accumulating frame queue.
Reuse the team app and backend The wardrobe shares navigation, theme, server configuration and deployment instead of maintaining a separate prototype application.
Structured garment data in MySQL, photos in file storage Editable attributes and measurements remain queryable through Django, while image bytes stay outside database rows. S3 integration is not implemented yet.

1.6 Login and onboarding — Hyeon U Jeong

Authentication is part of the existing Android/Django system. Compose screens observe account-aware ViewModels; repositories use the same backend and separate device storage by guest/account identity. Onboarding reuses the body-analysis and wardrobe flows instead of introducing another service or database.

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","edgeLabelBackground":"#000000","fontFamily":"Arial","fontSize":"16px"},"flowchart":{"curve":"stepAfter"}}}%%
flowchart TB
    UI["Auth screens / My Profile<br/>OnboardingRoute"]
    VM["AuthViewModel<br/>BodyProfileViewModel<br/>GuestImportViewModel"]
    AUTH["AuthRepository<br/>AccountAccess"]
    IMPORT["GuestImportRepository"]
    LOCAL["LocalUserStore<br/>guest / account scope"]
    KEY["KeystoreSessionStore<br/>encrypted session"]
    UI --> VM
    VM --> AUTH
    VM --> IMPORT
    VM --> LOCAL
    IMPORT --> LOCAL
    IMPORT -->|"account-bound requests"| AUTH
    AUTH --> KEY
    API["users: authentication / profile / import<br/>wardrobe and outfits: protected account APIs"]
    ANALYSIS["Existing body / garment analysis<br/>reused during optional onboarding"]
    DB[("MySQL<br/>User / AuthToken / AuthRateLimit<br/>Garment / Outfit / OutfitItem")]
    FILES[("Server garment photos")]
    DEVICE[("App-private snapshot and garment photos")]
  AUTH -->|"Bearer token"| API
  VM --> ANALYSIS
  API -->|"owner-scoped access"| DB
  API --> FILES
  LOCAL --> DEVICE
  classDef default fill:#333333,stroke:#FFFFFF,color:#FFFFFF
  linkStyle default stroke:#FFFFFF,color:#FFFFFF
Loading

Diagram image

External libraries and services

Component Purpose
Jetpack Compose, Lifecycle ViewModel and StateFlow Render profile/authentication/onboarding state and bind it to the current identity.
OkHttp and Kotlin coroutines Execute account API requests, propagate failures and cancel stale work.
Android Keystore, AES-GCM and AtomicFile Encrypt and atomically store the login session outside backup/device transfer.
Django authentication primitives and ORM Hash/check passwords, model owners/tokens, and perform transactional updates.
MySQL and existing server photo storage Persist accounts, tokens, garment/outfit ownership and imported photos.

Body analysis, garment landmarks and Gemini analysis retain their existing roles. This feature does not add a separate authentication service or change their algorithms.

Architectural decisions

Decision Rationale
Optional account from the start Users can complete or skip onboarding and save guest data without registration.
One guest/account snapshot store LocalUserStore holds the existing local data model in separate directories. No parallel legacy database or migration-only compatibility layer was added.
DB-backed opaque Bearer tokens The existing Django/MySQL stack supports expiration, per-token logout and shared validation without adding another session service.
Account-bound request handles Each AccountAccess is tied to the session that created it. An old request cannot overwrite or invalidate a newer login.
Explicit imports Login loads account data. Copying guest records requires a separate user action and conflict choice, avoiding unexpected uploads on another device.
Reuse body/wardrobe UI Optional setup uses the existing analyzers, editor and capture flow while keeping independent onboarding completion state.

2. Design Details (body analysis)

2.1 API

POST /api/body-profile/analyze/ · multipart/form-data

Field Required Value
front_photo, side_photo yes JPEG / PNG / WebP …, ≤ 8 MB each
height_cm yes 100–220
weight_kg no 30–200
gender no female / male / unspecified (default)

Responses:

Status Body When
200 analysis_id, inputs, pipeline_version, measurements[] (type, value_cm, confidence), derived ratios, warnings[], clothing (top/bottom: fitted/loose + probabilities), reference (true measurements, benchmark photos only, else null) analysis succeeded
422 error, photo (front/side), hint (Korean retake hint) invalid_image, no_person, multiple_people, body_cropped, not_frontal, not_side_view, not_upright, front_photo_required, side_photo_required
400 error: invalid_field, field, hint height, weight or gender out of range
413 error: photo_too_large a photo over 8 MB

Warning codes: loose_top, loose_bottom, no_weight, arms_touching_body, crotch_not_found. The app maps each to a Korean message.

2.2 Key data structures

classDiagram
  direction LR
  class BodyAnalyzer {
    <<interface>>
    +analyze(input, photos) AnalysisResult
  }
  class RemoteBodyAnalyzer {
    +parseResponse(status, body) AnalysisResult
  }
  class FakeBodyAnalyzer
  BodyAnalyzer <|.. RemoteBodyAnalyzer
  BodyAnalyzer <|.. FakeBodyAnalyzer

  class AnalysisInput {
    heightCm: Int
    weightKg: Int?
    gender: Gender
  }
  class AnalysisResult {
    measurements: List~BodyMeasurement~
    warnings: List~String~
    clothing: DetectedClothing
    reference: ReferenceMeasurements?
  }
  class BodyMeasurement {
    type: MeasurementType
    valueCm: Double
    confidence: Confidence
    editedByUser: Boolean
  }
  class DetectedClothing {
    topLoose: Boolean
    bottomLoose: Boolean
    affects(type) Boolean
  }
  class ReferenceMeasurements {
    body: String
    heightCm: Double
    values: Map
    accuracyPercent(m) Double?
  }
  class BodyProfile {
    input: AnalysisInput
    measurements: List~BodyMeasurement~
    preferredFit: PreferredFit
    preferredStyles: List~String~
    clothing: DetectedClothing
  }
  class BodyProfileViewModel {
    setup: StateFlow~SetupState~
    app: StateFlow~AppState~
    analyze()
    editMeasurement(type, cm)
    confirmProfile()
  }
  BodyAnalyzer ..> AnalysisResult
  AnalysisResult *-- BodyMeasurement
  AnalysisResult *-- DetectedClothing
  AnalysisResult o-- ReferenceMeasurements
  BodyProfile *-- AnalysisInput
  BodyProfile *-- BodyMeasurement
  BodyProfileViewModel --> BodyAnalyzer
  BodyProfileViewModel --> BodyProfile
Loading
  • MeasurementType: 16 values with a group (length / circumference), a Korean label and a base confidence. The server uses the same keys (shoulder_width, chest, …).
  • Confidence: high / medium / low. It is lowered for measurements under a detected loose garment and for low landmark visibility. Iteration 1 only shows it in the UI; it will stay internal later.
  • Server types (body_analysis/types.py) mirror these: AnalysisInput, Measurement, AnalysisResult, ClothingAssessment, AnalysisError (code → Korean hint).
  • Storage: confirmed body data is persisted through LocalBodyProfileStore / LocalUserStore. Guests keep it on the device; signed-in saves also update User.body_profile. Fit/mood/color preferences are stored separately from body data, and onboarding completion does not require a body profile (§4.4–§4.7).

2.3 Measurement pipeline (algorithm)

  1. Decode each photo in memory and shrink it to at most 1280 px on the long side.
  2. Benchmark check: if both photos carry the marker of one of our 8 synthetic benchmark bodies, keep that body's true measurements for the response and paint the marker out (§2.6).
  3. Pose and mask: MediaPipe Pose Landmarker (heavy) gives 33 landmarks and a person mask per photo. Images are padded to a width that is a multiple of 4, because MediaPipe aborts the process on other widths when reading the mask.
  4. Quality gate: reject photos with no person, several people, a body that is not upright, the wrong orientation (shoulder spread), or a cut-off body. Each case gets its own hint.
  5. Geometry:
    • Scale: entered height ÷ mask height in pixels.
    • Lengths: from landmarks and landmark-defined rows (waist = narrowest torso row, crotch, floor).
    • Circumferences: front width × side depth at the same relative body height, as an ellipse perimeter, times a per-measurement calibration factor.
  6. Clothing detector: two logistic regressions (loose top, loose bottom) on scale-free silhouette features.
  7. Learned correction: one ridge regression per measurement, on the same features plus height, BMI (if weight is given) and gender. It predicts measurement ÷ height. It is skipped for measurements under a detected loose garment, and the bicep is left out entirely under a loose top.
  8. Confidence, warnings and derived ratios (shoulder/hip, waist/hip, torso/leg) are added.

Both models were trained on 240 synthetic bodies rendered from the Anny 3D body model, with ground truth sliced from the mesh. They are exported as small JSON files and run with NumPy only, so the server needs no ML framework.

2.4 Insight rules (app)

buildInsights (data/BodyAnalyzer.kt) turns measurements into cards:

  • Body type:
    • 하체 볼륨형: hip/chest ≥ 1.10
    • 상체 볼륨형: chest/hip ≥ 1.10
    • 허리 라인형: waist/hip ≤ 0.70
    • 일자형: waist/hip ≥ 0.80
  • Leg proportion: inseam/height ≥ 0.47 or ≤ 0.45.
  • Shoulders: shoulder/height ≥ 0.265.

Each threshold has a margin about the size of the measurement error, so nothing is shown near it. A rule is skipped when a measurement it uses comes from a region with detected loose clothing, unless the user edited that value.

2.5 Body figure (app)

BodyFigureGeometry.kt is pure Kotlin and unit-tested. It holds:

  • the figure proportions per gender
  • the hotspot of each measurement (a band, a line or a point)
  • tap hit-testing: the nearest hotspot within about 26 dp

BodyFigure.kt draws the figure with Compose Canvas, pins the 5 key values as chips with leader lines, and shows the tapped measurement in a detail card.

2.6 Benchmark accuracy display

For demos and validation, the app can show real accuracy ("정확도 98.7 %" = 100 − relative error) for our benchmark bodies:

  • scripts/export_benchmark_reference.py renders the 8 bodies with a small marker in the top-left corner: a magenta start block, 5 bits for the body number and view, and a parity block. It also writes their true measurements to models/benchmark_reference.json.
  • The server returns reference only when both photos carry markers of the same body. Ordinary photos never get an accuracy.

A marker was chosen over image matching because two benchmark bodies differ by only a few centimetres. After the app's JPEG compression and downscaling, their images were closer to each other than two copies of the same photo. The marker is painted out before analysis, so it changes no measurement.

2.7 Implementation decisions

Topic Decision
Concurrency MediaPipe's landmarker is not thread-safe. Each process holds one pipeline, created on first use (the model takes ~1 s to load), and analyses run under a lock.
Memory ~340 MiB per process after the first analysis. The Kubernetes limit is 1 Gi (it was OOM-killed at 256 Mi).
Non-ASCII paths The model is loaded from bytes (model_asset_buffer), because MediaPipe's loader cannot open paths with Korean characters.
Photo handling in the app Photos are decoded at most 1280 px on the long side and rotated upright using EXIF, then re-encoded as JPEG, which drops metadata such as location. They are cleared from the ViewModel as soon as the analysis succeeds. A photo that cannot be read keeps the previous one and shows a message instead of crashing.
Network OkHttp timeouts: 10 s connect, 60 s read and write (the first request loads the model). The base URL comes from BuildConfig.BODY_API_BASE_URL.
Error contract The server owns all user-facing retake hints (Korean hint in 422 responses), so new rejection reasons need no app update.
Model files Learned models are versioned JSON in the repository (clothing-1, ridge-2). The pose model is downloaded and checksum-pinned at image build time. pipeline_version records which models produced a result.

3. Design Details (wardrobe)

3.1 API

All routes are under /api/wardrobe/.

Method and route Input Result
POST landmarks/?garment={type} Raw JPEG / PNG / WebP, ≤1 MiB and ≤1 million pixels Model identifier, garment type, normalized points, suggested measurement paths and processing time
POST analyze-preview/ Guest raw JPEG / PNG / WebP, ≤5 MiB and ≤12 million pixels Returns analysis attributes/details without creating a server draft; the app stores the guest draft/photo locally
POST analyze/ Owner Bearer token; raw JPEG / PNG / WebP, ≤5 MiB and ≤12 million pixels Creates an owner-linked analysis draft; returns id, model, attributes
GET options/ — Allowed codes, Korean labels, color palette and measurement definitions
GET items/ Owner Bearer token; optional after cursor Owner's saved items, newest first, with next pagination (§4.5)
GET items/{id}/ Garment UUID Current garment record, including a draft when its ID is known
PUT items/{id}/ JSON: attributes, dimensions, notes; ≤16 KiB Replaces the editable values and marks the same record as saved
GET items/{id}/image/ Saved garment UUID Stored JPEG

Image requests use raw image bytes, not multipart forms. Invalid input, rejected photos, busy services, missing configuration and timeouts produce error responses that the app maps to messages.

options/, landmarks/ and guest analyze-preview/ do not require MySQL. analyze/, item reads/updates and stored-photo reads require a valid Bearer token and an owner-scoped database record. Missing/expired credentials return 401; a foreign item ID returns 404. Guest wardrobe reads/saves use LocalUserStore instead of the account persistence API.

3.2 Key data structures

The diagrams show the main classes and modules used by the wardrobe. Nullability and secondary members are omitted; the database and API constraints are specified below.

Registration and persistence

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","edgeLabelBackground":"#000000","fontFamily":"Arial","fontSize":"16px"}}}%%
classDiagram
  direction LR
  %% Key members only. Nullability is omitted from this structural summary.

  class WardrobeViewModel {
    +state: StateFlow~WardrobeState~
    +refresh()
    +save(id, payload)
  }
  class WardrobeState {
    +records: List~JSONObject~
    +catalog: JSONObject
    +loading: Boolean
    +saving: Boolean
  }
  class WardrobeRepository {
    <<object>>
    +json(path, body) JSONObject
    +thumbnail(path) ImageBitmap
  }
  class GarmentAnalysisViewModel {
    +state: StateFlow~AnalysisState~
    +select(photo)
    +analyze(context, photo)
  }
  class AnalysisState {
    +photo: String
    +busy: Boolean
    +result: JSONObject
  }
  class GarmentAnalysisModule["GarmentAnalysis.kt"] {
    <<module>>
    requestAnalysis(context, photo) JSONObject
  }
  class WardrobeViews["apps/wardrobe/views.py"] {
    <<module>>
    analyze(request)
    items(request)
    item(request, item_id)
    item_image(request, item_id)
  }
  class Garment {
    <<Django model>>
    +id: UUID
    +image: FileField
    +attributes: JSON
    +dimensions: JSON
    +notes: Text
    +saved: Boolean
  }

  WardrobeViewModel --> WardrobeState : maintains
  WardrobeViewModel ..> WardrobeRepository : uses
  GarmentAnalysisViewModel --> AnalysisState : maintains
  GarmentAnalysisViewModel ..> GarmentAnalysisModule : calls
  WardrobeRepository ..> WardrobeViews : REST
  GarmentAnalysisModule ..> WardrobeViews : POST analyze
  WardrobeViews ..> Garment : creates / queries / updates
Loading

Capture and measurement

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","edgeLabelBackground":"#000000","fontFamily":"Arial","fontSize":"16px"}}}%%
classDiagram
  direction LR
  %% Key members only. Nullability is omitted from this structural summary.

  class MeasurementSnapshot {
    +bitmap: Bitmap
    +geometry: MeasurementGeometry
    +point(u, v) MeasurePoint
  }
  class MeasurementGeometry {
    -inverseViewProjection: FloatArray
    -planeOrigin: MeasurePoint
    -planeNormal: MeasurePoint
    +point(u, v) MeasurePoint
  }
  class MeasurePoint {
    +x: Float
    +y: Float
    +z: Float
  }
  class GarmentLandmarks {
    <<object>>
    +detect(bitmap, garment) LandmarkResult
  }
  class LandmarkResult {
    +suggestions: Map~String, LandmarkPath~
  }
  class LandmarkPath {
    +points: List~LandmarkPoint~
  }
  class LandmarkPoint {
    +x: Float
    +y: Float
  }

  MeasurementSnapshot --> MeasurementGeometry : contains
  MeasurementGeometry ..> MeasurePoint : uses / creates
  GarmentLandmarks ..> LandmarkResult : creates
  LandmarkResult --> LandmarkPath : suggestions
  LandmarkPath --> LandmarkPoint : points
Loading

UML diagram image

Database and data contract

Django stores one garment per row in wardrobe_garment.

Column Type Purpose
id UUID, primary key Stable garment identifier
user_id Required foreign key to User Owning account; item and photo APIs enforce this scope
image FileField Relative JPEG path
attributes JSON Current garment attributes
dimensions JSON, nullable Measurements and their sources
notes Text User memo; API limit 2,000 characters
saved Boolean, indexed false for an analysis draft; true after confirmation
created_at, updated_at DateTime Creation and update timestamps

attributes contains:

Field Contract
name Required; 1–80 characters after trimming
category top, bottom, outerwear, shoes
colors Distinct uppercase #RRGGBB values; AI suggests 1–2, user saves 0–5
subcategory Applicable type code or null
styles Up to three allowed style codes
fit_type Optional garment design fit
leg_shape, rise_type, skirt_shape Optional, category-dependent details

Gemini supplies only name, category and colors. Other fields start as null or empty arrays and can be set by the user. Pattern, relative length, material and wearing properties are not stored as attributes.

dimensions uses cm and stores each available measurement with its source and method. For example:

{
  "unit": "cm",
  "shoulder_width": {
    "value": 45.32,
    "source": "arcore_assisted",
    "method": "flat_shoulder_seam_to_seam",
    "reference": null
  }
}

This is a payload example, not an accuracy result.

Garment group Measurements
Tops / outerwear Shoulder width, half chest width, total length, sleeve length, half hem width, cuff width, straight armhole length
Trousers / shorts Half waist, hip and thigh widths; front rise; inseam; total length; single-leg hem width
Skirts Half waist width, half hip width, total length
Shoes No clothing measurements

Values must be finite and positive and are rounded to two decimal places. Missing measurements are omitted; an empty measurement set becomes null. Widths are not circumferences.

Sources distinguish automatic-point AR measurement, manual-point AR measurement and user-entered numbers. Editing a number changes its source to user_measured. The schema also accepts product_chart, but importing product size charts is not implemented.

Only the current attributes are stored; there is no separate copy of the original AI result. Image bytes, landmark coordinates, camera matrices and body profiles are not stored in the garment row.

3.3 Measurement and analysis pipeline

  1. Track the floor. ARCore estimates the camera pose and a horizontal plane. The user lays the garment flat and keeps it within the frame. No fixed capture height is required.
  2. Find image points. The backend resizes the image with aspect ratio preserved and runs pretrained GarmentIQ HRNet through ONNX Runtime on CPU. The selected garment type determines the landmark channels and measurement paths.
  3. Restore coordinates. Model coordinates are mapped back to the original image. Low-confidence or missing points may leave some measurements unavailable. Some endpoints are derived from predicted contours rather than directly predicted landmarks.
  4. Calculate cm on the phone. The app freezes the image and its AR camera and plane data together. A ray through each image point is intersected with the plane. Distances between intersections, or sums along a multi-point path, are converted from meters to centimeters.
  5. Review and describe. The user checks or replaces points. After photo confirmation, Gemini suggests the name, category and colors.
  6. Edit and save. The app combines the suggested attributes with compatible measurements and user edits, then saves the record.

HRNet is used without additional training or fine-tuning. It predicts landmark locations, not exact seam segmentation. The measurement assumes the garment lies on the tracked plane; folds, thickness, tracking errors and misplaced points can affect the result.

3.4 Persistence and implementation decisions

Decision Implementation
Draft before confirmation Signed-in analysis stores a normalized JPEG and an owner-linked row with saved=false; guest preview analysis creates its draft/photo only in local storage. Only confirmed saved=true records appear in the active wardrobe.
Update the same record Account confirmation and edits use authenticated PUT with the existing UUID; guests update the same UUID locally. Repeating a save does not create another garment. Repeating analysis can create another draft.
Keep image handling explicit EXIF orientation is applied, metadata is removed, and the image is stored as a JPEG with a maximum long edge of 1536 pixels.
Bound preview work The app allows one in-flight landmark request and discards results older than 1.2 seconds or inconsistent with the current camera pose.
Bound server inference Landmark detection and Gemini analysis share one process-local semaphore. A busy process returns 429 instead of accumulating requests. The ONNX session is loaded lazily and reused.
Bundle the model reproducibly The Docker build downloads pinned source and weights, verifies checksums, exports ONNX and includes the model, manifest and license in the final image. PyTorch is used only during the build.
Keep secrets on the backend Gemini and MySQL credentials come from backend environment configuration. They are not embedded in the Android app.

Abandoned drafts are not automatically expired or deleted. User ownership is enforced for account records and stored photos. Manual guest imports use version checks to detect changes after review (§4.6); ordinary garment edits do not provide general version-based conflict resolution, and garment deletion remains outside this feature. Server photo storage must persist independently of disposable application containers.


4. Design Details (login and onboarding)

This section documents the implemented account/guest integration, including session ownership, local storage, explicit imports and onboarding state. Body and garment analysis algorithms remain in §§2–3.

4.1 Authentication and profile API

Requests use JSON except garment-photo imports. Release builds keep cleartext traffic disabled; HTTP is enabled only in the debug manifest for local development. Protected routes require Authorization: Bearer <token>. Missing, expired, revoked or inactive-account credentials return 401; another owner's detail ID returns 404.

Method and route Input Result
POST /api/auth/register/ username, password, password_confirmation 201: token, expiry and public user data; registration also logs in.
POST /api/auth/login/ username, password 200: token, expiry and public user data.
POST /api/auth/logout/ Current Bearer token 204: revoke only this token.
GET /api/users/me/ Current Bearer token 200: public user data.
PATCH /api/users/me/ Any subset of body_profile, preferred_fits, preferred_styles, favorite_colors Update supplied profile fields and return public user data. Identity and password cannot be changed here.
  • IDs accept lowercase ASCII letters, digits and underscores, 4–20 characters. Password input is 8–64 characters; confirmation must match. Passwords are hashed with Django's password facilities and never included in public user responses.
  • Authentication/profile JSON is limited to 8 KiB. Unknown fields, duplicate keys, non-finite numbers and invalid nested body fields are rejected.
  • Profile lists contain distinct allowed values: up to five fits, six moods and eight colors. Empty confirmed fits become ["regular"]; empty moods/colors are valid. Colors are normalized to uppercase #RRGGBB.
  • body_profile is nullable and accepts height_cm, weight_kg, gender, measurements, clothing, and reference. Measurements use the existing 16 type codes, a positive finite cm value, optional confidence and a user-edited flag. Body photos are not stored in this field. The existing input screen accepts height 100–220 cm; profile API validation permits 100–230 cm.
  • Error codes include INVALID_CREDENTIALS, USERNAME_TAKEN, PASSWORD_MISMATCH, INVALID_PROFILE and BODY_TOO_LARGE. Unknown IDs and wrong passwords share INVALID_CREDENTIALS.

4.2 Session lifecycle and request limits

  1. Generate a cryptographically random 256-bit token on successful registration/login. Return its URL-safe text once; store only its SHA-256 hash, owner and expiration on the server.
  2. Persist the app session in noBackupFilesDir/auth-session.enc, encrypted with an Android Keystore AES-256-GCM key. The session contains the token, expiry and cached public profile, not the password.
  3. Bind account requests to an AccountAccess for that session. Validate the token on every protected server request and apply owner filters.
  4. Restore the local session at app startup and check the profile on foreground entry. Local expiry or a protected 401 clears that session and switches to guest UI. Network/server failure alone keeps the session.
  5. Logout revokes the current server token before clearing the local session. A failed logout remains retryable. Other issued tokens remain valid until their own logout or expiration.

Tokens expire 30 days after issuance; there is no sliding expiry or refresh-token endpoint. AuthRateLimit shares counters in the DB: five failed logins for an ID within five minutes block that ID for five minutes; registration/login requests share a 20-per-minute IP limit. A successful login clears that ID's failure counter. 429 responses provide Retry-After and retry_after.

The IP limit uses REMOTE_ADDR and does not trust client-supplied forwarded headers. Proxy address handling must be checked in the deployment environment so unrelated users do not share one proxy-address limit.

4.3 Key data structures

Only principal members are shown. The diagrams distinguish interfaces and classes; nullable/secondary members are omitted from the structural view.

Authentication and session ownership

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","fontFamily":"Arial","fontSize":"16px"}}}%%
classDiagram
  direction TB
  class AuthViewModel {
    +state: StateFlow~AuthUiState~
    +submit(username, password, confirmation)
    +refreshSession()
    +logout()
  }
  class AuthRepository {
    +status: StateFlow~AuthStatus~
    +restore()
    +authenticate(username, password, confirmation)
    +verify()
    +logout()
    +account() AccountAccess
  }
  class AccountAccess {
    +userId: String
    +json(method, path, data) JSONObject
    +list(path, key) JSONArray
    +patchProfile(data)
    +upload(path, metadata, image)
  }
  class AuthSessionStore {
    <<interface>>
    +read() String
    +write(value)
    +clear()
  }
  class KeystoreSessionStore {
    +read() String
    +write(value)
    +clear()
  }
  class AuthSession {
    +expiresAt: Instant
    +user: AccountUser
  }
  class AccountUser {
    +id: String
    +username: String
    +body: JSONObject
    +style: StyleProfile
  }
  AuthViewModel --> AuthRepository
  AuthRepository --> AuthSessionStore
  AuthSessionStore <|.. KeystoreSessionStore
  AuthRepository --> AuthSession
  AuthRepository ..> AccountAccess
  AccountAccess --> AuthSession : bound to login
  AccountAccess --> AuthRepository
  AuthSession --> AccountUser
Loading

Diagram image

Onboarding, local storage and imports

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","fontFamily":"Arial","fontSize":"16px"}}}%%
classDiagram
  direction TB
  class BodyProfileViewModel {
    +onboarding: StateFlow~OnboardingProgress~
    +moveOnboarding(step)
    +confirmOnboardingFits()
    +startOnboardingGarment(category, id)
    +skipSetup()
  }
  class BodyProfileStore {
    <<interface>>
    +load() BodyProfile
    +save(profile)
    +saveStyle(style)
    +saveOnboarding(progress)
  }
  class LocalBodyProfileStore
  class LocalUserStore {
    +style() StyleProfile
    +onboarding() OnboardingProgress
    +garments() List~JSONObject~
    +saveGarment(id, payload) JSONObject
    +recordImport(accountId, key, fingerprint, status)
  }
  class OnboardingProgress {
    +step: Int
    +completed: Boolean
    +draft: String
  }
  class GuestImportViewModel {
    +state: StateFlow~GuestImportState~
    +open()
    +confirm()
    +decide(replace)
    +retry()
  }
  class GuestImportRepository {
    +snapshot() GuestImportSnapshot
    +preview(snapshot) GuestImportPlan
    +importRecord(record, plan, replace) String
  }
  class AccountAccess
  BodyProfileViewModel --> BodyProfileStore
  BodyProfileStore <|.. LocalBodyProfileStore
  LocalBodyProfileStore --> LocalUserStore
  BodyProfileViewModel --> OnboardingProgress
  LocalUserStore ..> OnboardingProgress
  GuestImportViewModel --> GuestImportRepository
  GuestImportRepository --> LocalUserStore : import receipts
  GuestImportRepository --> AccountAccess : authenticated imports
Loading

Diagram image

StyleMateApp creates a separate ViewModel store for each active identity and disposes the previous one on a switch. BodyProfileStore is an interface: its local implementation adapts LocalUserStore; its in-memory implementation is used in tests/previews. OnboardingProgress(step, completed, draft) is local state independent of BodyProfile.

4.4 Database changes and data contract

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#000000","primaryColor":"#333333","primaryTextColor":"#FFFFFF","primaryBorderColor":"#FFFFFF","lineColor":"#FFFFFF","secondaryColor":"#333333","tertiaryColor":"#333333","textColor":"#FFFFFF","fontFamily":"Arial","fontSize":"16px"}}}%%
erDiagram
  User ||--o{ Garment : ""
  User ||--o{ Outfit : ""
  Outfit ||--o{ OutfitItem : ""
  Garment ||--o{ OutfitItem : ""
  User ||--o{ AuthToken : ""
  User {
    UUID id PK
    string username UK
    string password
    JSON body_profile
    JSON preferred_fits
    JSON preferred_styles
    JSON favorite_colors
  }
  Garment {
    UUID id PK
    UUID user_id FK
    string image
    JSON attributes
    JSON dimensions
    JSON labels
    bool saved
  }
  Outfit {
    UUID id PK
    UUID user_id FK
    string source
    bool saved
  }
  OutfitItem {
    bigint id PK
    UUID outfit_id FK
    UUID garment_id FK
    string slot
    string note
  }
  AuthToken {
    string key_hash PK
    UUID user_id FK
    datetime expires_at
  }
  AuthRateLimit {
    string key PK
    int count
    datetime window_started
    datetime blocked_until
  }
Loading

Diagram image

User and authentication tables

Table / field Type Purpose
users_user.id UUID, PK Internal account identity.
username Char(20), unique Exact app login ID.
password Char(128) Django-encoded password hash, not plaintext input.
last_login DateTime, nullable Most recent successful login.
body_profile JSON, nullable Existing body measurement structure without photos.
preferred_fits JSON list oversized, wide, regular, tapered, slim; default ["regular"].
preferred_styles JSON list minimal, cityboy, casual, street, classic, sporty; default [].
favorite_colors JSON list At most eight distinct uppercase HEX colors; default [].
is_active Boolean Whether the account may authenticate.
created_at, updated_at DateTime Account creation/update timestamps.
users_authtoken key_hash Char(64) PK; user_id FK; created_at; indexed expires_at Per-login server credential record.
users_authratelimit key Char(64) PK; nonnegative count; window_started; nullable blocked_until Hashed scope/identifier keys for shared request and failure counters.

Existing wardrobe/outfit relations

  • wardrobe_garment.user_id and outfits_outfit.user_id are required foreign keys to User. Garment attributes, measurements, labels and photo paths retain their existing structures.
  • outfits_outfititem continues to connect an Outfit and a Garment with slot and note. Its (outfit_id, slot) unique constraint is retained. Current slots are top, bottom and optional outerwear; shoes remain registrable garments rather than outfit members.
  • Neither User nor Outfit stores garment/outfit IDs as a replacement array. Small preference arrays stay in User JSON fields; relational ownership and membership use foreign keys.
  • API code enforces ownership and one top/one bottom with optional outerwear. Foreign-key existence and unique outfit slots are DB constraints; cross-record category/ownership validation is application logic.
  • Garments referenced by any stored outfit, including an unsaved recommendation, cannot change category. Ordinary saves and import replacements return 409 GARMENT_CATEGORY_IN_USE; same-category edits remain allowed. Garment/category updates and outfit creation use the same user-row transaction lock. Recommendation results are rechecked before persistence in case a garment changed while calculating.
  • Django deletion policies cascade account removal to its garments, outfits and tokens; there is no account-deletion UI/API in this feature.

Migration: add the User/token/limit tables, add garment/outfit owner foreign keys, remove legacy unowned garment/outfit rows, and make the foreign keys required. This intentionally approved one-time cleanup preserves owned records; it is not an ongoing deletion policy for new user data.

4.5 Local persistence and account caching

LocalUserStore saves private metadata under filesDir/local-users/guest/data.json or filesDir/local-users/<account UUID>/data.json. Guest garment photos are kept under that directory's garments/<UUID>.jpg path. The snapshot holds body/style data, onboarding progress/drafts, recommendation taste, garments/outfits and per-account import receipts.

Writes use a temporary file, flush/sync and atomic replacement. Reading malformed data reports an error instead of resetting the file. The implementation uses the new store directly; it does not preserve an incompatible old database through a second storage layer.

Account profile and wardrobe metadata are cached separately from guest data. Server-backed account saves require connectivity; current account photo and saved-outfit reads are online. Guest saved records are local, but new body/garment analysis and recommendation calculation still require the backend.

Account garment and saved-outfit lists use next and ?after=<next>. Each page contains at most 20 records and at most 128 KiB of serialized item data, leaving room under the Android 256 KiB response limit. The URL-safe cursor carries the account/list scope and the page boundary's timestamp/ID; queries always remain owner-filtered. The boundary does not move if the previous page's last record is edited or unsaved. This is not a transaction-wide snapshot of every record during concurrent edits.

The app treats the cursor as opaque, validates its length/character set and rejects repeated cursors. Only a complete successful wardrobe fetch replaces its cache. Guest recommendation previews omit notes, image paths and analysis caches and use a matching 1 MiB request limit on app/server; source records remain unchanged.

4.6 Explicit guest imports

Method and route Input Result
POST /api/users/me/import/preview/ Profile candidates and guest garment/outfit UUIDs; up to 100 IDs of each kind per batch Existing-record conflicts and current versions.
PUT /api/users/me/import/profile/{body|style}/ value, replace, expected Keep, confirm unchanged or import body/style.
POST /api/users/me/import/garments/{guest UUID}/ Multipart JSON metadata and image Import a garment and normalized photo.
PUT /api/users/me/import/outfits/{guest UUID}/ value, replace, expected Import an outfit after mapping its garments to the account.
  1. Snapshot the explicit guest candidates: confirmed body/style data, saved garments/photos and saved outfits. Starting login never starts this operation.
  2. Preview conflicts and versions in bounded batches. Keep account values by default; selected conflicts opt into replacement.
  3. Map each guest garment/outfit UUID to a deterministic server UUID using UUIDv5 over the account UUID, record kind and source UUID. Equal names are not identity, and two accounts cannot share the imported row.
  4. Import profile sections and garments before outfits. Validate that every outfit member resolves to a saved garment owned by this account with the correct category/slot.
  5. Lock the user/target row in a transaction and compare the expected version. A changed account value returns 409 IMPORT_CHANGED instead of overwriting the reviewed data. Repeating the same value after a lost response returns an unchanged result.
  6. Record successful imported, unchanged or kept receipts and content fingerprints locally. Failures remain pending, source data is retained, and profile-refresh failure after a successful upload can be retried without uploading it again. A later manual import may revisit records previously kept.

Import JSON is bounded to 32 KiB per operation. Photos are JPEG/PNG/WebP, at most 5 MiB and 12 million pixels, and pass through the existing image-normalization path. Account-linked garments retain the category-change restriction above. Imports create no extra DB model and do not recompute recommendation results.

4.7 Onboarding orchestration and interaction decisions

Step State / action Persistence and transition
1 Welcome Start, optional login, skip. Start → 2; skip completes local setup.
2 Fit Multiple choices, including an empty draft. Persist the draft independently; next/skip confirms regular only if empty, then → 3.
3 Mood Multiple mood choices. Save style independently of body data; next/skip → 4.
4 Body input Existing photo/height/optional weight/gender input. Save numbers, never body photos. Analyze → 5; skip → 6.
5 Results Existing result review and measurement edits. Confirm saves the body profile and → 6 without completing onboarding.
6 Wardrobe setup Three equal-width counts with vertical dividers; top/bottom/shoe actions. Add/edit → 7; next/skip → 8. Suggested counts are not gates.
7 Garment registration Existing photo capture/analysis/editor, including shoe photo registration. Persist editor draft; save/cancel/skip → 6. Restore analyzed local photos; retake if a temporary capture was removed.
8 Completion Style/garment summary, first outfit entry, optional login. Mark local completion before entering the main app or authentication.

The shared header has back, StyleMate and one upper-right skip action. There is no duplicate lower skip action. My Profile toggles fits/moods directly, commits color edits explicitly and opens body results through the body-balance card. These screens reuse the existing theme and do not introduce another typography system.

The first recommendation uses the existing feature: missing body information shows its analysis-required guidance, and missing suitable top/bottom garments shows its registration guidance. Onboarding stores preferences in User after account save/import, but this change does not add new recommendation rules for those fields.

4.8 Failure handling and implementation boundaries

Condition Handling
Duplicate authentication/import submission Busy state blocks duplicate actions.
Token expires or an old session responds late Clear only the matching session; a newer identity is retained.
Offline login/account save/import Show a retryable error; preserve session/input/source data as applicable.
Bad local snapshot Report the read failure and retain the existing file.
Invalid garment UUID or foreign garment in outfit creation Return 400 INVALID_OUTFIT; do not create an outfit.
Category change would invalidate an outfit Return 409 GARMENT_CATEGORY_IN_USE; do not partially modify metadata/photo.
Import review becomes stale Return 409 IMPORT_CHANGED; require review again.
Account list page fails Retain the prior cache rather than publishing a partial list.

The feature uses the existing backend, MySQL connection configuration and photo-storage deployment. Its new deployment-specific checks are applying the added migrations and verifying proxy IP handling for the new authentication request limit. Local USB forwarding is a development connection method, not a production requirement. Social authentication, password recovery, continuous guest/account synchronization and new outfit/chat algorithms are outside this change.

5. Design Patterns

TODO (as a team). Candidates in body analysis: Strategy (PoseEstimator / BodyAnalyzer interfaces with real and fake implementations) and a lazily created shared pipeline in services.py.

⚠️ **GitHub.com Fallback** ⚠️