Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ Upgrading from a release before sign-in identities (the `user_identities` table)
| Variable | Description | Required |
|----------|-------------|----------|
| `BG_REMOVAL_PROVIDER` | Background removal backend: `rembg` or `http` (default: `rembg`) | No |
| `BG_REMOVAL_MODEL` | rembg model name (default: `u2net`) | No |
| `BG_REMOVAL_MODEL` | rembg model name (default: `birefnet-general-lite`) | No |
| `BG_REMOVAL_URL` | URL for HTTP bg removal provider | If http |
| `BG_REMOVAL_API_KEY` | API key for HTTP bg removal provider | No |
| `NEXT_PUBLIC_ENABLE_IP_LOCATION_FALLBACK` | Enable IP-based approximate location when browser geolocation is denied/unavailable. Off by default (sends the user's IP to a third party). Set to `true` to enable | No |
Expand Down Expand Up @@ -400,12 +400,12 @@ If browser geolocation is denied or unavailable, an optional fallback can approx

### Background Removal

Works out of the box: the backend image ships rembg with the default `u2net` model already downloaded.
Works out of the box: the backend image ships rembg with the default `birefnet-general-lite` model already downloaded. It is more accurate on clothes than `u2net`, and the cut-out is cleaned afterwards: small holes the model punched into a print are filled back in, loose bits of background are dropped, and a result that lost the garment entirely keeps the original photo.

<details>
<summary>Other rembg models, or an HTTP provider</summary>

Change the rembg model with `BG_REMOVAL_MODEL` (options: `isnet-general-use`, `silueta`, `u2netp`). A non-default model is downloaded the first time background removal runs, so that first run needs internet access; the compose files keep it in the `rembg_models` volume so later runs work offline. On Kubernetes, mount a PersistentVolume at `/opt/rembg/models` on the image-worker if you change the model, or it is downloaded again on every pod start. The volume must be writable by uid 1000: the manifests' `fsGroup` covers a PersistentVolumeClaim, but a `hostPath` volume needs its directory owned by that uid. The worker links the baked `u2net` into the volume on start, so switching back needs no download.
Change the rembg model with `BG_REMOVAL_MODEL` (options: `u2net`, `isnet-general-use`, `silueta`, `u2netp`). A non-default model is downloaded the first time background removal runs, so that first run needs internet access; the compose files keep it in the `rembg_models` volume so later runs work offline. On Kubernetes, mount a PersistentVolume at `/opt/rembg/models` on the image-worker if you change the model, or it is downloaded again on every pod start. The volume must be writable by uid 1000: the manifests' `fsGroup` covers a PersistentVolumeClaim, but a `hostPath` volume needs its directory owned by that uid. The worker links the baked `birefnet-general-lite` into the volume on start, so switching back needs no download.

To use an HTTP provider (e.g. [withoutbg](https://github.com/nicholasgasior/withoutbg)) instead:

Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ RUN pip install --no-cache-dir -r requirements.txt -r requirements-extras.txt
# Importing rembg JIT-compiles pymatting through numba for over a minute, so the bake also fills
# the numba cache. A generic CPU target keeps those entries valid on hosts other than the builder,
# and the cache stays writable by every uid because numba refuses a cache dir it cannot write to.
ARG BG_REMOVAL_MODEL=u2net
ARG BG_REMOVAL_MODEL=birefnet-general-lite
ENV REMBG_HOME=/opt/rembg \
NUMBA_CACHE_DIR=/opt/numba_cache \
NUMBA_CPU_NAME=generic
Expand Down
25 changes: 25 additions & 0 deletions backend/app/api/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from app.models.user import User
from app.utils.auth import get_current_user
from app.utils.clothing import WardrobeComposition, count_composition
from app.utils.garment_vocabulary import CATEGORIES, TYPE_CATEGORY
from app.utils.signed_urls import sign_optional

router = APIRouter(prefix="/analytics", tags=["Analytics"])
Expand All @@ -29,6 +30,28 @@ class TypeDistribution(BaseModel):
percentage: float


class CategoryDistribution(BaseModel):
category: str
count: int
percentage: float


def category_distribution(types: list[TypeDistribution]) -> list[CategoryDistribution]:
"""Clothing, footwear and accessories totals; every section is listed, even when empty."""
counts = dict.fromkeys(CATEGORIES, 0)
for entry in types:
counts[TYPE_CATEGORY.get(entry.type, "clothing")] += entry.count
total = sum(counts.values())
return [
CategoryDistribution(
category=category,
count=count,
percentage=round(count / total * 100, 1) if total else 0,
)
for category, count in counts.items()
]


class WearStats(BaseModel):
id: UUID
name: str | None
Expand Down Expand Up @@ -108,6 +131,7 @@ class AnalyticsResponse(BaseModel):
wardrobe: WardrobeStats
color_distribution: list[ColorDistribution]
type_distribution: list[TypeDistribution]
category_distribution: list[CategoryDistribution] = []
most_worn: list[WearStats]
least_worn: list[WearStats]
never_worn: list[WearStats]
Expand Down Expand Up @@ -431,6 +455,7 @@ def wear_stats_query(order_desc: bool, limit: int, never_worn: bool = False):
wardrobe=wardrobe_stats,
color_distribution=color_distribution,
type_distribution=type_distribution,
category_distribution=category_distribution(type_distribution),
most_worn=most_worn,
least_worn=least_worn,
never_worn=never_worn,
Expand Down
Loading
Loading