Ox1 Cursor commited on
Commit
295e5db
·
1 Parent(s): b269444

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>

Files changed (2) hide show
  1. app.py +178 -27
  2. 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
- # Sample wardrobe handler
172
  # ---------------------------------------------------------------------------
173
 
174
- SAMPLES_DIR = Path(__file__).resolve().parent / "data" / "samples"
175
  DATA_DIR = Path(__file__).resolve().parent / "data"
176
 
 
 
 
 
177
 
178
- def load_sample_wardrobe():
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
- dest = DATA_DIR / "garments"
186
- dest.mkdir(parents=True, exist_ok=True)
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
- catalog = load_catalog()
193
- return f"Cargadas **{len(catalog)}** prendas de ejemplo. Ve a 'Mi Armario' para explorarlas."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- sample_btn = gr.Button(
467
- "Cargar armario de ejemplo (50 prendas)",
468
- variant="secondary", size="sm",
 
 
 
 
 
469
  )
470
- sample_status = gr.Markdown()
471
 
472
- sample_btn.click(
473
- load_sample_wardrobe,
474
- outputs=[sample_status],
 
 
 
 
 
 
 
 
 
 
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 Fashion-1K dataset.
2
 
3
- Downloads flat lay images from Codatta/Fashion-1K (HuggingFace),
4
- runs the full detection + VLM pipeline, and saves the results to
5
- data/samples/ for instant loading in the demo.
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
- TARGET_GARMENTS = 50
36
- MAX_IMAGES_TO_PROCESS = 40
37
-
38
- # Indices hand-picked from Fashion-1K for variety (tops, bottoms, dresses,
39
- # outerwear, shoes, accessories). If these don't yield enough garments,
40
- # the script will continue with sequential images.
41
- CURATED_INDICES = [
 
 
 
 
 
 
 
 
 
 
 
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 process_image(image: Image.Image, image_idx: int, catalog: list, garment_counter: int) -> int:
59
- """Process a single source image through detection + VLM.
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 main():
108
- logger.info("=== Building Sample Wardrobe ===")
109
- logger.info("Target: %d garments from Fashion-1K", TARGET_GARMENTS)
 
 
110
 
111
- try:
112
- from datasets import load_dataset
113
- except ImportError:
114
- logger.error("'datasets' package not installed. Run: pip install datasets")
115
- sys.exit(1)
116
 
117
- logger.info("Loading Codatta/Fashion-1K dataset...")
118
- ds = load_dataset("Codatta/Fashion-1K", split="train")
119
- logger.info("Dataset loaded: %d images", len(ds))
120
 
121
- SAMPLES_DIR.mkdir(parents=True, exist_ok=True)
122
- GARMENTS_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
123
 
 
 
 
 
 
124
  catalog: list[dict] = []
125
  garment_counter = 0
126
- images_processed = 0
127
 
128
- # Process curated indices first
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) ---", images_processed, idx)
148
- garment_counter = process_image(image, idx, catalog, garment_counter)
149
 
150
- # If we haven't reached target, try sequential
151
  if garment_counter < TARGET_GARMENTS:
152
- processed_indices = set(CURATED_INDICES)
153
  for idx in range(len(ds)):
154
  if garment_counter >= TARGET_GARMENTS:
155
  break
156
- if idx in processed_indices:
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 %d (dataset idx %d) ---", images_processed, idx)
170
- garment_counter = process_image(image, idx, catalog, 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
- # Print summary by type
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")