Spaces:
Runtime error
Runtime error
File size: 5,735 Bytes
75e0882 14e7ce6 75e0882 913537b 75e0882 913537b 75e0882 913537b 75e0882 14e7ce6 226ca6c 14e7ce6 226ca6c 75e0882 226ca6c 75e0882 226ca6c 14e7ce6 226ca6c 14e7ce6 75e0882 226ca6c 75e0882 226ca6c 75e0882 6ca17ee 75e0882 6ca17ee 75e0882 14e7ce6 75e0882 14e7ce6 75e0882 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """Catalog module: JSON-based wardrobe storage with search.
The catalog is a simple JSON file that stores garment entries. Each
garment has a unique ID and structured attributes extracted by the
vision pipeline.
No vector DB, no embeddings. In-memory filtering over a list of dicts.
A wardrobe will never exceed a few hundred items.
"""
import json
import logging
from pathlib import Path
from .storage import save_image, get_image_path, delete_image
logger = logging.getLogger(__name__)
CATALOG_PATH = Path(__file__).resolve().parent.parent / "data" / "catalog.json"
def load_catalog() -> list[dict]:
"""Load the catalog from disk. Returns empty list if file doesn't exist."""
if not CATALOG_PATH.exists():
logger.debug("Catalog file not found: %s", CATALOG_PATH)
return []
try:
with open(CATALOG_PATH, "r", encoding="utf-8") as f:
catalog = json.load(f)
logger.debug("Loaded catalog: %d garments from %s", len(catalog), CATALOG_PATH)
return catalog
except (json.JSONDecodeError, IOError) as e:
logger.error("Failed to load catalog: %s", e)
return []
def save_catalog(garments: list[dict]) -> None:
"""Write the full catalog to disk."""
CATALOG_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(CATALOG_PATH, "w", encoding="utf-8") as f:
json.dump(garments, f, indent=2, ensure_ascii=False)
logger.info("Catalog saved: %d garments", len(garments))
def _next_id(catalog: list[dict]) -> int:
"""Get the next available garment ID number."""
if not catalog:
return 1
existing_ids = []
for g in catalog:
gid = g.get("id", "")
if isinstance(gid, str) and gid.startswith("garment_"):
try:
existing_ids.append(int(gid.split("_")[1]))
except (ValueError, IndexError):
pass
return max(existing_ids, default=0) + 1
def add_garments(
garments_with_images: list[tuple[dict, bytes | None]],
) -> list[dict]:
"""Add new garments to the catalog. Assigns IDs and persists images.
Accepts a list of (garment_dict, image_bytes) tuples. Each garment
gets its own individual image saved.
Returns the garments with their assigned IDs.
"""
catalog = load_catalog()
next_num = _next_id(catalog)
added = []
for i, (garment, img_bytes) in enumerate(garments_with_images):
garment_id = f"garment_{next_num + i:03d}"
garment["id"] = garment_id
if img_bytes:
filename = save_image(garment_id, img_bytes)
garment["image_ref"] = filename
catalog.append(garment)
added.append(garment)
save_catalog(catalog)
return added
def search(
garment_type: str | None = None,
color: str | None = None,
season: str | None = None,
formality: str | None = None,
) -> list[dict]:
"""Filter catalog by attributes. All filters are AND-combined."""
catalog = load_catalog()
results = catalog
if garment_type:
garment_type = garment_type.lower().strip()
results = [g for g in results if garment_type in g.get("type", "")]
if color:
color = color.lower().strip()
results = [g for g in results if color in g.get("color", "")]
if season:
season = season.lower().strip()
results = [g for g in results if g.get("season", "") in (season, "all")]
if formality:
formality = formality.lower().strip()
results = [g for g in results if formality in g.get("formality", "")]
return results
def get_catalog_summary() -> str:
"""Format the catalog as a text block for LLM context injection."""
catalog = load_catalog()
if not catalog:
return "The wardrobe is empty. No garments have been cataloged yet."
lines = [f"Wardrobe catalog ({len(catalog)} garments):\n"]
for g in catalog:
gid = g.get("id", "?")
gtype = g.get("type", "?")
color = g.get("color", "?")
material = g.get("material", "?")
pattern = g.get("pattern", "?")
season = g.get("season", "?")
formality = g.get("formality", "?")
description = g.get("description", "")
line = (
f"- [{gid}] {color} {gtype} ({material}, {pattern}) "
f"| season: {season} | style: {formality}"
)
if description:
line += f"\n → {description}"
lines.append(line)
return "\n".join(lines)
def get_catalog_stats() -> dict:
"""Return aggregate statistics about the catalog."""
catalog = load_catalog()
if not catalog:
return {"total": 0}
types: dict[str, int] = {}
colors: dict[str, int] = {}
seasons: dict[str, int] = {}
for g in catalog:
t = g.get("type", "unknown")
types[t] = types.get(t, 0) + 1
c = g.get("color", "unknown")
colors[c] = colors.get(c, 0) + 1
s = g.get("season", "unknown")
seasons[s] = seasons.get(s, 0) + 1
return {
"total": len(catalog),
"by_type": dict(sorted(types.items(), key=lambda x: -x[1])),
"by_color": dict(sorted(colors.items(), key=lambda x: -x[1])),
"by_season": dict(sorted(seasons.items(), key=lambda x: -x[1])),
}
def get_garment_image_path(garment_id: str) -> str | None:
"""Resolve the local filesystem path for a garment's image."""
return get_image_path(garment_id)
def clear_catalog() -> None:
"""Remove all garments from the catalog and their images."""
catalog = load_catalog()
for g in catalog:
gid = g.get("id", "")
if gid:
delete_image(gid)
save_catalog([])
logger.info("Catalog cleared")
|