Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0a1a169
fix: skip outfit options missing a top, bottom or shoes and fill them…
Anyesh Oct 12, 2026
6335e25
fix: rank fill-ins by fit and wear, and drop picks that are no longer…
Anyesh Oct 12, 2026
5692d34
fix: require a real top, not a coat alone, and never fill an answer t…
Anyesh Oct 12, 2026
6ae5804
fix: fill with a top before a hoodie, respect slots, and allow a dres…
Anyesh Oct 12, 2026
7450564
fix: only demote a hoodie for a real top and pick the partial the fil…
Anyesh Oct 12, 2026
5594bfe
fix: choose the fill plan that dresses the most regions instead of fi…
Anyesh Oct 12, 2026
79e89f1
fix: rank a top swapped in for a hoodie at the hoodie's rank
Anyesh Oct 12, 2026
9b890b4
test: keep the hoodie-then-shirt case where only region count saves t…
Anyesh Oct 12, 2026
abcedf4
fix: log filled regions in body order
Anyesh Oct 12, 2026
02025ab
fix: count a suit as a bottom so it still needs a top, and share regi…
Anyesh Oct 16, 2026
e66e73d
test: name the suit fill cases after the bottom-only suit rule
Anyesh Oct 16, 2026
c743a7f
fix: rank fill-ins by season too, sharing the fit factors with score_…
Anyesh Oct 17, 2026
3848e5e
fix: count only a layer as wearable in a wardrobe that dresses no region
Anyesh Oct 17, 2026
77ec4e6
fix: refuse an outfit the fill cannot complete instead of saving it
Anyesh Oct 17, 2026
b6ffd3e
fix: record which pieces the fill added in the saved AI response
Anyesh Oct 17, 2026
a8f7017
fix: refuse requested items the wardrobe cannot complete before calli…
Anyesh Oct 17, 2026
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
2 changes: 1 addition & 1 deletion backend/app/data/garment_vocabulary.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{"value": "jacket", "role": "outer_layer", "wash_interval": 8},
{"value": "coat", "role": "outer_layer", "wash_interval": 10},
{"value": "sweater", "role": "base_top", "wash_interval": 5},
{"value": "hoodie", "role": "outer_layer", "wash_interval": 4},
{"value": "hoodie", "role": "outer_layer", "wash_interval": 4, "worn_alone": true},
{"value": "blazer", "role": "outer_layer", "wash_interval": 5},
{"value": "suit", "role": "suit", "wash_interval": 5},
{"value": "vest", "role": "mid_layer", "wash_interval": 5},
Expand Down
44 changes: 40 additions & 4 deletions backend/app/services/item_scorer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import date
from math import prod
from statistics import median
from uuid import UUID

Expand Down Expand Up @@ -264,6 +265,42 @@ def _formality_score(item: ClothingItem, occasion: str) -> float:
return 0.15


def _fit_factors(
item: ClothingItem,
weather: WeatherData,
occasion: str,
current_season: str,
preferences: UserPreference | None,
learned_prefs: dict | None,
) -> tuple[float, float, float, float]:
return (
_weather_score(item, weather, preferences),
_formality_score(item, occasion),
_season_score(item, current_season, weather, preferences),
_preference_score(item, preferences, learned_prefs),
)


def rank_by_fit(
items: list[ClothingItem],
weather: WeatherData,
occasion: str,
current_season: str,
preferences: UserPreference | None,
learned_prefs: dict | None,
) -> list[UUID]:
# score_items leaves small wardrobes unscored, so filling a missing piece needs its own
# ranking or it would fall back to database order. Most items tie on fit in mild
# weather, so the least recently worn goes first to avoid adding the same piece every time.
def key(item: ClothingItem) -> tuple[float, date]:
fit = prod(
_fit_factors(item, weather, occasion, current_season, preferences, learned_prefs)
)
return -fit, item.last_worn_at or date.min

return [item.id for item in sorted(items, key=key)]


def _season_score(
item: ClothingItem,
current_season: str,
Expand Down Expand Up @@ -456,11 +493,10 @@ def score_items(

scored = []
for item in items:
ws = _weather_score(item, weather, preferences)
fs = _formality_score(item, occasion)
ss = _season_score(item, current_season, weather, preferences)
ws, fs, ss, ps = _fit_factors(
item, weather, occasion, current_season, preferences, learned_prefs
)
rs = _recency_score(item, user_today, avoid_days, recently_worn_dates)
ps = _preference_score(item, preferences, learned_prefs)
us = _usage_score(item, median_wear) if use_underused else 1.0

total = ws * fs * ss * rs * ps * us
Expand Down
6 changes: 3 additions & 3 deletions backend/app/services/pairing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from app.models.outfit import FamilyOutfitRating, Outfit, OutfitItem, OutfitSource, OutfitStatus
from app.models.user import User
from app.services.ai_service import AIResponseTruncatedError, AIService, require_internal_ai
from app.utils.clothing import covered_body_slots, deduplicate_by_body_slot
from app.utils.clothing import covered_regions, deduplicate_by_body_slot
from app.utils.garment_vocabulary import render_outfit_prompt
from app.utils.prompts import load_prompt
from app.utils.timezone import get_user_today
Expand Down Expand Up @@ -270,13 +270,13 @@ async def generate_pairings(
# mandatory: a full-body garment claims both the top and bottom slots first, so
# without this the source would be dropped and the pairing persisted without the
# item it is supposed to feature.
picked_slots = covered_body_slots(valid_ids, item_type_map)
picked_regions = covered_regions(valid_ids, item_type_map)
valid_ids = deduplicate_by_body_slot(
valid_ids, item_type_map, mandatory_item_ids={source_item.id}
)
# Dedup must not leave the top or bottom bare where the model had covered it: a
# dress dropped for a shirt source would leave the outfit with no bottom.
if covered_body_slots(valid_ids, item_type_map) != picked_slots:
if covered_regions(valid_ids, item_type_map) != picked_regions:
logger.warning("Pairing silhouette does not fit the source item, skipping")
continue

Expand Down
Loading
Loading