Design Documentation - snuhcs-course/swpp-2026-project-team-14 GitHub Wiki
| 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. |
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
| 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/. |
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
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.
| 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) |
| 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. |
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
| 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 |
| 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. |
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
| 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.
| 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. |
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.
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
-
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 updateUser.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).
- Decode each photo in memory and shrink it to at most 1280 px on the long side.
- 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).
- 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.
- 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.
-
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.
- Clothing detector: two logistic regressions (loose top, loose bottom) on scale-free silhouette features.
- 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.
- 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.
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.
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.
For demos and validation, the app can show real accuracy ("정확도 98.7 %" = 100 − relative error) for our benchmark bodies:
-
scripts/export_benchmark_reference.pyrenders 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 tomodels/benchmark_reference.json. - The server returns
referenceonly 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.
| 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. |
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.
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.
%%{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
%%{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
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.
- 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.
- 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.
- 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.
- 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.
- Review and describe. The user checks or replaces points. After photo confirmation, Gemini suggests the name, category and colors.
- 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.
| 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.
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.
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_profileis nullable and acceptsheight_cm,weight_kg,gender,measurements,clothing, andreference. 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_PROFILEandBODY_TOO_LARGE. Unknown IDs and wrong passwords shareINVALID_CREDENTIALS.
- 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.
- 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. - Bind account requests to an
AccountAccessfor that session. Validate the token on every protected server request and apply owner filters. - Restore the local session at app startup and check the profile on foreground entry. Local expiry or a protected
401clears that session and switches to guest UI. Network/server failure alone keeps the session. - 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.
Only principal members are shown. The diagrams distinguish interfaces and classes; nullable/secondary members are omitted from the structural view.
%%{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
%%{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
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.
%%{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
}
| 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. |
-
wardrobe_garment.user_idandoutfits_outfit.user_idare required foreign keys to User. Garment attributes, measurements, labels and photo paths retain their existing structures. -
outfits_outfititemcontinues to connect an Outfit and a Garment withslotandnote. 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.
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.
| 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. |
- Snapshot the explicit guest candidates: confirmed body/style data, saved garments/photos and saved outfits. Starting login never starts this operation.
- Preview conflicts and versions in bounded batches. Keep account values by default; selected conflicts opt into replacement.
- 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.
- 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.
- Lock the user/target row in a transaction and compare the expected version. A changed account value returns
409 IMPORT_CHANGEDinstead of overwriting the reviewed data. Repeating the same value after a lost response returns an unchanged result. - Record successful
imported,unchangedorkeptreceipts 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.
| 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.
| 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.
TODO (as a team). Candidates in body analysis: Strategy (
PoseEstimator/BodyAnalyzerinterfaces with real and fake implementations) and a lazily created shared pipeline inservices.py.