Spaces:
Runtime error
Runtime error
refactor(ui): replace subprocess sample build with in-process dataset loader
Browse files- New obtener_dataset() generator runs VLM pipeline in-process
- Real-time UI updates: log + gallery filling as garments are processed
- Supports multiple HF datasets (second-hand, fashion-1k)
- Single "Obtener Dataset" button replaces previous two-button setup
- Add event context textbox in Combina tab for LLM ranking
- Detect HF Spaces environment for server binding (0.0.0.0:7860)
- Ensure data/ directories are created at startup
Co-authored-by: Cursor <cursoragent@cursor.com>
- app.py +178 -27
- scripts/build_sample_wardrobe.py +140 -49
app.py
CHANGED
|
@@ -9,7 +9,6 @@ Built for the Build Small Hackathon (HuggingFace x Gradio, June 2026).
|
|
| 9 |
import io
|
| 10 |
import logging
|
| 11 |
import os
|
| 12 |
-
import shutil
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
from dotenv import load_dotenv
|
|
@@ -19,7 +18,7 @@ load_dotenv(Path(__file__).resolve().parent / ".env")
|
|
| 19 |
import gradio as gr
|
| 20 |
from PIL import Image
|
| 21 |
|
| 22 |
-
from src.vision import extract_garments, extract_single_from_path, extract_from_crop_bytes
|
| 23 |
from src.catalog import (
|
| 24 |
add_garments,
|
| 25 |
load_catalog,
|
|
@@ -31,6 +30,7 @@ from src.catalog import (
|
|
| 31 |
from src.assistant import ask_streaming
|
| 32 |
from src.combinations import (
|
| 33 |
generate_combinations,
|
|
|
|
| 34 |
save_preference,
|
| 35 |
get_liked_outfits,
|
| 36 |
)
|
|
@@ -41,6 +41,10 @@ from gradio_image_annotation import image_annotator
|
|
| 41 |
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
|
| 42 |
logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
_startup_catalog = load_catalog()
|
| 45 |
logging.info("Startup: catalog loaded with %d garments", len(_startup_catalog))
|
| 46 |
del _startup_catalog
|
|
@@ -168,29 +172,149 @@ def update_detection_backend(backend_name: str):
|
|
| 168 |
|
| 169 |
|
| 170 |
# ---------------------------------------------------------------------------
|
| 171 |
-
#
|
| 172 |
# ---------------------------------------------------------------------------
|
| 173 |
|
| 174 |
-
SAMPLES_DIR = Path(__file__).resolve().parent / "data" / "samples"
|
| 175 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
| 179 |
-
"""Load the pre-processed sample wardrobe into the active catalog."""
|
| 180 |
-
if not SAMPLES_DIR.exists() or not (SAMPLES_DIR / "catalog.json").exists():
|
| 181 |
-
return "Datos de ejemplo no disponibles. Ejecuta scripts/build_sample_wardrobe.py primero."
|
| 182 |
|
| 183 |
-
shutil.copy(SAMPLES_DIR / "catalog.json", DATA_DIR / "catalog.json")
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
src_garments = SAMPLES_DIR / "garments"
|
| 188 |
-
if src_garments.exists():
|
| 189 |
-
for img in src_garments.glob("*.jpg"):
|
| 190 |
-
shutil.copy(img, dest / img.name)
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
# ---------------------------------------------------------------------------
|
|
@@ -339,8 +463,8 @@ def _get_combo_display(combo: dict | None) -> tuple[str | None, str | None, str,
|
|
| 339 |
return top_img, bottom_img, top_text, bottom_text
|
| 340 |
|
| 341 |
|
| 342 |
-
def init_combinations(state):
|
| 343 |
-
"""Initialize or refresh the combination queue."""
|
| 344 |
combos = generate_combinations()
|
| 345 |
if not combos:
|
| 346 |
return (
|
|
@@ -351,6 +475,9 @@ def init_combinations(state):
|
|
| 351 |
"0 combinaciones",
|
| 352 |
)
|
| 353 |
|
|
|
|
|
|
|
|
|
|
| 354 |
state = {"queue": combos, "index": 0}
|
| 355 |
combo = combos[0]
|
| 356 |
top_img, bottom_img, top_text, bottom_text = _get_combo_display(combo)
|
|
@@ -462,16 +589,31 @@ with gr.Blocks(title="Wardrobe AI") as demo:
|
|
| 462 |
)
|
| 463 |
|
| 464 |
gr.Markdown("---")
|
|
|
|
| 465 |
with gr.Row():
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
)
|
| 470 |
-
sample_status = gr.Markdown()
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
)
|
| 476 |
|
| 477 |
with gr.Accordion("⚙️ Ajustes de detección", open=False):
|
|
@@ -570,6 +712,12 @@ with gr.Blocks(title="Wardrobe AI") as demo:
|
|
| 570 |
|
| 571 |
combo_state = gr.State(value=None)
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
with gr.Row():
|
| 574 |
generate_btn = gr.Button(
|
| 575 |
"Generar combinaciones", variant="primary", size="lg",
|
|
@@ -614,7 +762,7 @@ with gr.Blocks(title="Wardrobe AI") as demo:
|
|
| 614 |
|
| 615 |
generate_btn.click(
|
| 616 |
init_combinations,
|
| 617 |
-
inputs=[combo_state],
|
| 618 |
outputs=combo_outputs,
|
| 619 |
)
|
| 620 |
like_btn.click(
|
|
@@ -646,7 +794,10 @@ Construido para el [Build Small Hackathon](https://huggingface.co/build-small-ha
|
|
| 646 |
|
| 647 |
|
| 648 |
if __name__ == "__main__":
|
|
|
|
| 649 |
demo.launch(
|
|
|
|
|
|
|
| 650 |
theme=gr.themes.Soft(
|
| 651 |
primary_hue="stone",
|
| 652 |
secondary_hue="amber",
|
|
|
|
| 9 |
import io
|
| 10 |
import logging
|
| 11 |
import os
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
from dotenv import load_dotenv
|
|
|
|
| 18 |
import gradio as gr
|
| 19 |
from PIL import Image
|
| 20 |
|
| 21 |
+
from src.vision import extract_garments, extract_single_from_path, extract_from_crop_bytes, _extract_single_garment
|
| 22 |
from src.catalog import (
|
| 23 |
add_garments,
|
| 24 |
load_catalog,
|
|
|
|
| 30 |
from src.assistant import ask_streaming
|
| 31 |
from src.combinations import (
|
| 32 |
generate_combinations,
|
| 33 |
+
rank_with_llm,
|
| 34 |
save_preference,
|
| 35 |
get_liked_outfits,
|
| 36 |
)
|
|
|
|
| 41 |
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
|
| 42 |
logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
|
| 43 |
|
| 44 |
+
_DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 45 |
+
_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 46 |
+
(_DATA_DIR / "garments").mkdir(parents=True, exist_ok=True)
|
| 47 |
+
|
| 48 |
_startup_catalog = load_catalog()
|
| 49 |
logging.info("Startup: catalog loaded with %d garments", len(_startup_catalog))
|
| 50 |
del _startup_catalog
|
|
|
|
| 172 |
|
| 173 |
|
| 174 |
# ---------------------------------------------------------------------------
|
| 175 |
+
# Dataset loading handler (in-process with real-time progress)
|
| 176 |
# ---------------------------------------------------------------------------
|
| 177 |
|
|
|
|
| 178 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 179 |
|
| 180 |
+
SAMPLE_DATASETS = [
|
| 181 |
+
("second-hand", "Second-hand (prendas individuales)"),
|
| 182 |
+
("fashion-1k", "Fashion-1K (multi-garment, requiere detección)"),
|
| 183 |
+
]
|
| 184 |
|
| 185 |
+
TARGET_GARMENTS = 50
|
|
|
|
|
|
|
|
|
|
| 186 |
|
|
|
|
| 187 |
|
| 188 |
+
def obtener_dataset(dataset_key: str):
|
| 189 |
+
"""Download a HF dataset and process garments in-process with real-time UI updates.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
+
Yields (log_markdown, gallery_images) at each step.
|
| 192 |
+
"""
|
| 193 |
+
from datasets import load_dataset
|
| 194 |
+
|
| 195 |
+
ds_configs = {
|
| 196 |
+
"second-hand": {
|
| 197 |
+
"hf_id": "fnauman/fashion-second-hand-front-only-rgb",
|
| 198 |
+
"needs_detection": False,
|
| 199 |
+
},
|
| 200 |
+
"fashion-1k": {
|
| 201 |
+
"hf_id": "Codatta/Fashion-1K",
|
| 202 |
+
"needs_detection": True,
|
| 203 |
+
},
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
config = ds_configs.get(dataset_key)
|
| 207 |
+
if not config:
|
| 208 |
+
yield "Error: dataset no reconocido.", []
|
| 209 |
+
return
|
| 210 |
+
|
| 211 |
+
yield f"Descargando dataset **{config['hf_id']}**...", []
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
ds = load_dataset(config["hf_id"], split="train")
|
| 215 |
+
except Exception as e:
|
| 216 |
+
yield f"Error descargando dataset: {e}", []
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
yield f"Dataset cargado: **{len(ds)}** imágenes. Iniciando procesamiento...", []
|
| 220 |
+
|
| 221 |
+
# Spread indices for variety
|
| 222 |
+
step = max(1, len(ds) // (TARGET_GARMENTS * 2))
|
| 223 |
+
indices = list(range(0, len(ds), step))[:TARGET_GARMENTS * 3]
|
| 224 |
+
|
| 225 |
+
gallery_images: list[str] = []
|
| 226 |
+
garments_processed: list[tuple[dict, bytes]] = []
|
| 227 |
+
log_lines: list[str] = []
|
| 228 |
+
|
| 229 |
+
for idx in indices:
|
| 230 |
+
if len(garments_processed) >= TARGET_GARMENTS:
|
| 231 |
+
break
|
| 232 |
+
if idx >= len(ds):
|
| 233 |
+
continue
|
| 234 |
+
|
| 235 |
+
sample = ds[idx]
|
| 236 |
+
image = sample.get("image") or sample.get("img")
|
| 237 |
+
if not isinstance(image, Image.Image):
|
| 238 |
+
continue
|
| 239 |
+
if image.mode != "RGB":
|
| 240 |
+
image = image.convert("RGB")
|
| 241 |
+
|
| 242 |
+
if config["needs_detection"]:
|
| 243 |
+
import tempfile
|
| 244 |
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
| 245 |
+
image.save(tmp, format="JPEG", quality=90)
|
| 246 |
+
tmp_path = tmp.name
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
from src.detector import detect_and_crop
|
| 250 |
+
crops = detect_and_crop(tmp_path)
|
| 251 |
+
except Exception:
|
| 252 |
+
crops = []
|
| 253 |
+
finally:
|
| 254 |
+
Path(tmp_path).unlink(missing_ok=True)
|
| 255 |
+
|
| 256 |
+
if not crops:
|
| 257 |
+
continue
|
| 258 |
+
|
| 259 |
+
for crop_bytes in crops:
|
| 260 |
+
if len(garments_processed) >= TARGET_GARMENTS:
|
| 261 |
+
break
|
| 262 |
+
garment = _extract_single_garment(crop_bytes)
|
| 263 |
+
if garment:
|
| 264 |
+
garments_processed.append((garment, crop_bytes))
|
| 265 |
+
img_path = _save_temp_preview(crop_bytes, len(garments_processed))
|
| 266 |
+
if img_path:
|
| 267 |
+
gallery_images.append(img_path)
|
| 268 |
+
n = len(garments_processed)
|
| 269 |
+
log_lines.append(
|
| 270 |
+
f"**{n}/{TARGET_GARMENTS}** — {garment.get('color', '?')} {garment.get('type', '?')}"
|
| 271 |
+
)
|
| 272 |
+
yield "\n".join(log_lines[-8:]), gallery_images
|
| 273 |
+
else:
|
| 274 |
+
if max(image.size) > 512:
|
| 275 |
+
image.thumbnail((512, 512), Image.LANCZOS)
|
| 276 |
+
|
| 277 |
+
buf = io.BytesIO()
|
| 278 |
+
image.save(buf, format="JPEG", quality=90)
|
| 279 |
+
crop_bytes = buf.getvalue()
|
| 280 |
+
|
| 281 |
+
garment = _extract_single_garment(crop_bytes)
|
| 282 |
+
if not garment:
|
| 283 |
+
continue
|
| 284 |
+
|
| 285 |
+
garments_processed.append((garment, crop_bytes))
|
| 286 |
+
img_path = _save_temp_preview(crop_bytes, len(garments_processed))
|
| 287 |
+
if img_path:
|
| 288 |
+
gallery_images.append(img_path)
|
| 289 |
+
n = len(garments_processed)
|
| 290 |
+
log_lines.append(
|
| 291 |
+
f"**{n}/{TARGET_GARMENTS}** — {garment.get('color', '?')} {garment.get('type', '?')}"
|
| 292 |
+
)
|
| 293 |
+
yield "\n".join(log_lines[-8:]), gallery_images
|
| 294 |
+
|
| 295 |
+
if not garments_processed:
|
| 296 |
+
yield "No se pudieron extraer prendas del dataset.", gallery_images
|
| 297 |
+
return
|
| 298 |
+
|
| 299 |
+
# Save to catalog
|
| 300 |
+
added = add_garments(garments_processed)
|
| 301 |
+
final_log = "\n".join(log_lines[-5:]) + (
|
| 302 |
+
f"\n\n---\n**Completado: {len(added)} prendas** añadidas al armario. "
|
| 303 |
+
f"Ve a 'Mi Armario' para explorarlas."
|
| 304 |
+
)
|
| 305 |
+
yield final_log, gallery_images
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def _save_temp_preview(crop_bytes: bytes, index: int) -> str | None:
|
| 309 |
+
"""Save a preview image for the gallery during dataset loading."""
|
| 310 |
+
preview_dir = DATA_DIR / "garments"
|
| 311 |
+
preview_dir.mkdir(parents=True, exist_ok=True)
|
| 312 |
+
path = preview_dir / f"_preview_{index:03d}.jpg"
|
| 313 |
+
try:
|
| 314 |
+
path.write_bytes(crop_bytes)
|
| 315 |
+
return str(path)
|
| 316 |
+
except Exception:
|
| 317 |
+
return None
|
| 318 |
|
| 319 |
|
| 320 |
# ---------------------------------------------------------------------------
|
|
|
|
| 463 |
return top_img, bottom_img, top_text, bottom_text
|
| 464 |
|
| 465 |
|
| 466 |
+
def init_combinations(state, context):
|
| 467 |
+
"""Initialize or refresh the combination queue, optionally ranked by context."""
|
| 468 |
combos = generate_combinations()
|
| 469 |
if not combos:
|
| 470 |
return (
|
|
|
|
| 475 |
"0 combinaciones",
|
| 476 |
)
|
| 477 |
|
| 478 |
+
if context and context.strip():
|
| 479 |
+
combos = rank_with_llm(combos, context.strip())
|
| 480 |
+
|
| 481 |
state = {"queue": combos, "index": 0}
|
| 482 |
combo = combos[0]
|
| 483 |
top_img, bottom_img, top_text, bottom_text = _get_combo_display(combo)
|
|
|
|
| 589 |
)
|
| 590 |
|
| 591 |
gr.Markdown("---")
|
| 592 |
+
gr.Markdown("#### Obtener dataset de ejemplo")
|
| 593 |
with gr.Row():
|
| 594 |
+
dataset_select = gr.Dropdown(
|
| 595 |
+
choices=[(label, key) for key, label in SAMPLE_DATASETS],
|
| 596 |
+
value="second-hand",
|
| 597 |
+
label="Dataset de origen",
|
| 598 |
+
scale=2,
|
| 599 |
+
)
|
| 600 |
+
obtener_btn = gr.Button(
|
| 601 |
+
"Obtener Dataset", variant="primary", size="lg", scale=1,
|
| 602 |
)
|
|
|
|
| 603 |
|
| 604 |
+
dataset_log = gr.Markdown()
|
| 605 |
+
dataset_gallery = gr.Gallery(
|
| 606 |
+
label="Prendas procesadas",
|
| 607 |
+
columns=6,
|
| 608 |
+
rows=2,
|
| 609 |
+
height="auto",
|
| 610 |
+
object_fit="contain",
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
obtener_btn.click(
|
| 614 |
+
obtener_dataset,
|
| 615 |
+
inputs=[dataset_select],
|
| 616 |
+
outputs=[dataset_log, dataset_gallery],
|
| 617 |
)
|
| 618 |
|
| 619 |
with gr.Accordion("⚙️ Ajustes de detección", open=False):
|
|
|
|
| 712 |
|
| 713 |
combo_state = gr.State(value=None)
|
| 714 |
|
| 715 |
+
event_context = gr.Textbox(
|
| 716 |
+
placeholder="Ej: cena informal en terraza, 30 grados...",
|
| 717 |
+
label="Describe la ocasión (opcional — las combinaciones se ordenarán para este contexto)",
|
| 718 |
+
lines=1,
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
with gr.Row():
|
| 722 |
generate_btn = gr.Button(
|
| 723 |
"Generar combinaciones", variant="primary", size="lg",
|
|
|
|
| 762 |
|
| 763 |
generate_btn.click(
|
| 764 |
init_combinations,
|
| 765 |
+
inputs=[combo_state, event_context],
|
| 766 |
outputs=combo_outputs,
|
| 767 |
)
|
| 768 |
like_btn.click(
|
|
|
|
| 794 |
|
| 795 |
|
| 796 |
if __name__ == "__main__":
|
| 797 |
+
is_spaces = os.environ.get("SPACE_ID") is not None
|
| 798 |
demo.launch(
|
| 799 |
+
server_name="0.0.0.0" if is_spaces else "127.0.0.1",
|
| 800 |
+
server_port=7860,
|
| 801 |
theme=gr.themes.Soft(
|
| 802 |
primary_hue="stone",
|
| 803 |
secondary_hue="amber",
|
scripts/build_sample_wardrobe.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
| 1 |
-
"""Build a pre-processed sample wardrobe from
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
|
| 7 |
Usage:
|
| 8 |
cd packages/wardrobe-us
|
| 9 |
-
.venv/bin/python scripts/build_sample_wardrobe.py
|
|
|
|
| 10 |
|
| 11 |
Requires: datasets>=2.18.0 (pip install datasets)
|
| 12 |
"""
|
| 13 |
|
|
|
|
| 14 |
import io
|
| 15 |
import json
|
| 16 |
import logging
|
|
@@ -32,13 +34,24 @@ SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples"
|
|
| 32 |
GARMENTS_DIR = SAMPLES_DIR / "garments"
|
| 33 |
CATALOG_PATH = SAMPLES_DIR / "catalog.json"
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
0, 5, 12, 18, 25, 33, 41, 50, 58, 67,
|
| 43 |
75, 83, 91, 100, 110, 120, 130, 140, 150, 160,
|
| 44 |
170, 180, 190, 200, 220, 240, 260, 280, 300, 320,
|
|
@@ -55,11 +68,8 @@ def save_crop(garment_id: str, crop_bytes: bytes) -> str:
|
|
| 55 |
return filename
|
| 56 |
|
| 57 |
|
| 58 |
-
def
|
| 59 |
-
"""Process a
|
| 60 |
-
|
| 61 |
-
Returns the updated garment counter.
|
| 62 |
-
"""
|
| 63 |
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
| 64 |
image.save(tmp, format="JPEG", quality=90)
|
| 65 |
tmp_path = tmp.name
|
|
@@ -104,61 +114,68 @@ def process_image(image: Image.Image, image_idx: int, catalog: list, garment_cou
|
|
| 104 |
return garment_counter
|
| 105 |
|
| 106 |
|
| 107 |
-
def
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
sys.exit(1)
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
catalog: list[dict] = []
|
| 125 |
garment_counter = 0
|
| 126 |
-
|
| 127 |
|
| 128 |
-
|
| 129 |
-
for idx in CURATED_INDICES:
|
| 130 |
if garment_counter >= TARGET_GARMENTS:
|
| 131 |
break
|
| 132 |
if idx >= len(ds):
|
| 133 |
continue
|
| 134 |
-
|
| 135 |
-
images_processed += 1
|
| 136 |
-
if images_processed > MAX_IMAGES_TO_PROCESS:
|
| 137 |
break
|
| 138 |
|
| 139 |
sample = ds[idx]
|
| 140 |
image = sample["image"]
|
| 141 |
if not isinstance(image, Image.Image):
|
| 142 |
continue
|
| 143 |
-
|
| 144 |
if image.mode != "RGB":
|
| 145 |
image = image.convert("RGB")
|
| 146 |
|
| 147 |
-
logger.info("--- Processing image %d (dataset idx %d) ---",
|
| 148 |
-
garment_counter =
|
| 149 |
|
| 150 |
-
#
|
| 151 |
if garment_counter < TARGET_GARMENTS:
|
| 152 |
-
|
| 153 |
for idx in range(len(ds)):
|
| 154 |
if garment_counter >= TARGET_GARMENTS:
|
| 155 |
break
|
| 156 |
-
if idx in
|
| 157 |
continue
|
| 158 |
-
if images_processed >= MAX_IMAGES_TO_PROCESS:
|
| 159 |
-
break
|
| 160 |
|
| 161 |
-
images_processed += 1
|
| 162 |
sample = ds[idx]
|
| 163 |
image = sample["image"]
|
| 164 |
if not isinstance(image, Image.Image):
|
|
@@ -166,8 +183,83 @@ def main():
|
|
| 166 |
if image.mode != "RGB":
|
| 167 |
image = image.convert("RGB")
|
| 168 |
|
| 169 |
-
logger.info("--- Processing image
|
| 170 |
-
garment_counter =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
# Save catalog
|
| 173 |
with open(CATALOG_PATH, "w", encoding="utf-8") as f:
|
|
@@ -175,11 +267,10 @@ def main():
|
|
| 175 |
|
| 176 |
logger.info("=== Done ===")
|
| 177 |
logger.info("Total garments: %d", len(catalog))
|
| 178 |
-
logger.info("Images processed: %d", images_processed)
|
| 179 |
logger.info("Catalog saved: %s", CATALOG_PATH)
|
| 180 |
logger.info("Garment images: %s", GARMENTS_DIR)
|
| 181 |
|
| 182 |
-
#
|
| 183 |
types: dict[str, int] = {}
|
| 184 |
for g in catalog:
|
| 185 |
t = g.get("type", "unknown")
|
|
|
|
| 1 |
+
"""Build a pre-processed sample wardrobe from HuggingFace datasets.
|
| 2 |
|
| 3 |
+
Supports multiple dataset sources:
|
| 4 |
+
- fashion-1k: Codatta/Fashion-1K (flat lays, needs detection)
|
| 5 |
+
- second-hand: fnauman/fashion-second-hand-front-only-rgb (individual garments)
|
| 6 |
|
| 7 |
Usage:
|
| 8 |
cd packages/wardrobe-us
|
| 9 |
+
.venv/bin/python scripts/build_sample_wardrobe.py --dataset second-hand
|
| 10 |
+
.venv/bin/python scripts/build_sample_wardrobe.py --dataset fashion-1k
|
| 11 |
|
| 12 |
Requires: datasets>=2.18.0 (pip install datasets)
|
| 13 |
"""
|
| 14 |
|
| 15 |
+
import argparse
|
| 16 |
import io
|
| 17 |
import json
|
| 18 |
import logging
|
|
|
|
| 34 |
GARMENTS_DIR = SAMPLES_DIR / "garments"
|
| 35 |
CATALOG_PATH = SAMPLES_DIR / "catalog.json"
|
| 36 |
|
| 37 |
+
DEFAULT_TARGET = 50
|
| 38 |
+
TARGET_GARMENTS = DEFAULT_TARGET
|
| 39 |
+
|
| 40 |
+
DATASETS = {
|
| 41 |
+
"second-hand": {
|
| 42 |
+
"hf_id": "fnauman/fashion-second-hand-front-only-rgb",
|
| 43 |
+
"description": "31K individual garments on uniform background (no detection needed)",
|
| 44 |
+
"needs_detection": False,
|
| 45 |
+
},
|
| 46 |
+
"fashion-1k": {
|
| 47 |
+
"hf_id": "Codatta/Fashion-1K",
|
| 48 |
+
"description": "1K flat lay outfits (multi-garment, needs detection + cropping)",
|
| 49 |
+
"needs_detection": True,
|
| 50 |
+
},
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Curated indices for Fashion-1K variety
|
| 54 |
+
FASHION_1K_INDICES = [
|
| 55 |
0, 5, 12, 18, 25, 33, 41, 50, 58, 67,
|
| 56 |
75, 83, 91, 100, 110, 120, 130, 140, 150, 160,
|
| 57 |
170, 180, 190, 200, 220, 240, 260, 280, 300, 320,
|
|
|
|
| 68 |
return filename
|
| 69 |
|
| 70 |
|
| 71 |
+
def process_with_detection(image: Image.Image, image_idx: int, catalog: list, garment_counter: int) -> int:
|
| 72 |
+
"""Process a flat lay image through detection + VLM. Returns updated garment counter."""
|
|
|
|
|
|
|
|
|
|
| 73 |
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
| 74 |
image.save(tmp, format="JPEG", quality=90)
|
| 75 |
tmp_path = tmp.name
|
|
|
|
| 114 |
return garment_counter
|
| 115 |
|
| 116 |
|
| 117 |
+
def process_individual(image: Image.Image, image_idx: int, catalog: list, garment_counter: int) -> int:
|
| 118 |
+
"""Process a single-garment image directly with VLM (no detection needed)."""
|
| 119 |
+
buf = io.BytesIO()
|
| 120 |
+
image.save(buf, format="JPEG", quality=90)
|
| 121 |
+
crop_bytes = buf.getvalue()
|
| 122 |
|
| 123 |
+
garment = _extract_single_garment(crop_bytes)
|
| 124 |
+
if not garment:
|
| 125 |
+
logger.debug("Image %d: VLM extraction failed, skipping", image_idx)
|
| 126 |
+
return garment_counter
|
|
|
|
| 127 |
|
| 128 |
+
garment_counter += 1
|
| 129 |
+
garment_id = f"garment_{garment_counter:03d}"
|
| 130 |
+
garment["id"] = garment_id
|
| 131 |
|
| 132 |
+
image_ref = save_crop(garment_id, crop_bytes)
|
| 133 |
+
garment["image_ref"] = image_ref
|
| 134 |
+
|
| 135 |
+
catalog.append(garment)
|
| 136 |
+
logger.info(
|
| 137 |
+
" [%d/%d] %s: %s %s (%s)",
|
| 138 |
+
garment_counter, TARGET_GARMENTS,
|
| 139 |
+
garment_id, garment.get("color", "?"), garment.get("type", "?"),
|
| 140 |
+
garment.get("pattern", "?"),
|
| 141 |
+
)
|
| 142 |
|
| 143 |
+
return garment_counter
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def build_from_fashion_1k(ds) -> list[dict]:
|
| 147 |
+
"""Build sample wardrobe from Fashion-1K (multi-garment flat lays)."""
|
| 148 |
catalog: list[dict] = []
|
| 149 |
garment_counter = 0
|
| 150 |
+
max_images = 40
|
| 151 |
|
| 152 |
+
for i, idx in enumerate(FASHION_1K_INDICES):
|
|
|
|
| 153 |
if garment_counter >= TARGET_GARMENTS:
|
| 154 |
break
|
| 155 |
if idx >= len(ds):
|
| 156 |
continue
|
| 157 |
+
if i >= max_images:
|
|
|
|
|
|
|
| 158 |
break
|
| 159 |
|
| 160 |
sample = ds[idx]
|
| 161 |
image = sample["image"]
|
| 162 |
if not isinstance(image, Image.Image):
|
| 163 |
continue
|
|
|
|
| 164 |
if image.mode != "RGB":
|
| 165 |
image = image.convert("RGB")
|
| 166 |
|
| 167 |
+
logger.info("--- Processing image %d (dataset idx %d) ---", i + 1, idx)
|
| 168 |
+
garment_counter = process_with_detection(image, idx, catalog, garment_counter)
|
| 169 |
|
| 170 |
+
# Fill remaining with sequential if needed
|
| 171 |
if garment_counter < TARGET_GARMENTS:
|
| 172 |
+
processed = set(FASHION_1K_INDICES[:max_images])
|
| 173 |
for idx in range(len(ds)):
|
| 174 |
if garment_counter >= TARGET_GARMENTS:
|
| 175 |
break
|
| 176 |
+
if idx in processed:
|
| 177 |
continue
|
|
|
|
|
|
|
| 178 |
|
|
|
|
| 179 |
sample = ds[idx]
|
| 180 |
image = sample["image"]
|
| 181 |
if not isinstance(image, Image.Image):
|
|
|
|
| 183 |
if image.mode != "RGB":
|
| 184 |
image = image.convert("RGB")
|
| 185 |
|
| 186 |
+
logger.info("--- Processing image (dataset idx %d) ---", idx)
|
| 187 |
+
garment_counter = process_with_detection(image, idx, catalog, garment_counter)
|
| 188 |
+
|
| 189 |
+
return catalog
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def build_from_second_hand(ds) -> list[dict]:
|
| 193 |
+
"""Build sample wardrobe from second-hand dataset (individual garments)."""
|
| 194 |
+
catalog: list[dict] = []
|
| 195 |
+
garment_counter = 0
|
| 196 |
+
|
| 197 |
+
# Spread indices across the dataset for variety
|
| 198 |
+
step = max(1, len(ds) // (TARGET_GARMENTS * 2))
|
| 199 |
+
indices = list(range(0, len(ds), step))[:TARGET_GARMENTS * 2]
|
| 200 |
+
|
| 201 |
+
for i, idx in enumerate(indices):
|
| 202 |
+
if garment_counter >= TARGET_GARMENTS:
|
| 203 |
+
break
|
| 204 |
+
|
| 205 |
+
sample = ds[idx]
|
| 206 |
+
image = sample.get("image") or sample.get("img")
|
| 207 |
+
if not isinstance(image, Image.Image):
|
| 208 |
+
continue
|
| 209 |
+
if image.mode != "RGB":
|
| 210 |
+
image = image.convert("RGB")
|
| 211 |
+
|
| 212 |
+
# Resize large images to max 512px to save VLM time
|
| 213 |
+
if max(image.size) > 512:
|
| 214 |
+
image.thumbnail((512, 512), Image.LANCZOS)
|
| 215 |
+
|
| 216 |
+
logger.info("--- Processing image %d/%d (dataset idx %d) ---", i + 1, len(indices), idx)
|
| 217 |
+
garment_counter = process_individual(image, idx, catalog, garment_counter)
|
| 218 |
+
|
| 219 |
+
return catalog
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def main():
|
| 223 |
+
parser = argparse.ArgumentParser(description="Build sample wardrobe from HuggingFace dataset")
|
| 224 |
+
parser.add_argument(
|
| 225 |
+
"--dataset",
|
| 226 |
+
choices=list(DATASETS.keys()),
|
| 227 |
+
default="second-hand",
|
| 228 |
+
help="Dataset source to use (default: second-hand)",
|
| 229 |
+
)
|
| 230 |
+
parser.add_argument(
|
| 231 |
+
"--target",
|
| 232 |
+
type=int,
|
| 233 |
+
default=DEFAULT_TARGET,
|
| 234 |
+
help=f"Number of garments to generate (default: {DEFAULT_TARGET})",
|
| 235 |
+
)
|
| 236 |
+
args = parser.parse_args()
|
| 237 |
+
|
| 238 |
+
global TARGET_GARMENTS
|
| 239 |
+
TARGET_GARMENTS = args.target
|
| 240 |
+
|
| 241 |
+
ds_config = DATASETS[args.dataset]
|
| 242 |
+
logger.info("=== Building Sample Wardrobe ===")
|
| 243 |
+
logger.info("Dataset: %s (%s)", args.dataset, ds_config["description"])
|
| 244 |
+
logger.info("Target: %d garments", TARGET_GARMENTS)
|
| 245 |
+
|
| 246 |
+
try:
|
| 247 |
+
from datasets import load_dataset
|
| 248 |
+
except ImportError:
|
| 249 |
+
logger.error("'datasets' package not installed. Run: pip install datasets")
|
| 250 |
+
sys.exit(1)
|
| 251 |
+
|
| 252 |
+
logger.info("Loading %s...", ds_config["hf_id"])
|
| 253 |
+
ds = load_dataset(ds_config["hf_id"], split="train")
|
| 254 |
+
logger.info("Dataset loaded: %d images", len(ds))
|
| 255 |
+
|
| 256 |
+
SAMPLES_DIR.mkdir(parents=True, exist_ok=True)
|
| 257 |
+
GARMENTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 258 |
+
|
| 259 |
+
if args.dataset == "fashion-1k":
|
| 260 |
+
catalog = build_from_fashion_1k(ds)
|
| 261 |
+
else:
|
| 262 |
+
catalog = build_from_second_hand(ds)
|
| 263 |
|
| 264 |
# Save catalog
|
| 265 |
with open(CATALOG_PATH, "w", encoding="utf-8") as f:
|
|
|
|
| 267 |
|
| 268 |
logger.info("=== Done ===")
|
| 269 |
logger.info("Total garments: %d", len(catalog))
|
|
|
|
| 270 |
logger.info("Catalog saved: %s", CATALOG_PATH)
|
| 271 |
logger.info("Garment images: %s", GARMENTS_DIR)
|
| 272 |
|
| 273 |
+
# Summary by type
|
| 274 |
types: dict[str, int] = {}
|
| 275 |
for g in catalog:
|
| 276 |
t = g.get("type", "unknown")
|