romi2001 commited on
Commit
5227589
·
verified ·
1 Parent(s): 4d7db34

Create App.PY

Browse files
Files changed (1) hide show
  1. App.PY +435 -0
App.PY ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Fashion Stylist — Hugging Face Space app
3
+ Combines: CLIP recommendation engine (Part 3) + 3 GenAI patterns (Part 4):
4
+ 1. Small Language Model -> generates text (Qwen2.5-0.5B-Instruct)
5
+ 2. Small Vision-Language Model -> answers questions (BLIP-VQA-base)
6
+ 3. Small Vision-Language pipeline -> generates an image (clothing segmentation + SD inpainting)
7
+
8
+ Deploy notes:
9
+ - Upload this file + requirements.txt + final_image_embeddings.npy + catalog_metadata.parquet
10
+ to your HF Space repo root.
11
+ - Update HF_DATASET_REPO / HF_WINNING_MODEL below if your repo names differ.
12
+ - For Path A "Quick Starters" to actually 1-click work, add 2-3 sample photos to a
13
+ samples/ folder in your Space and update SAMPLE_PHOTOS below.
14
+ """
15
+
16
+ import urllib.parse
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ import torch
21
+ import gradio as gr
22
+ import faiss
23
+ from datasets import load_dataset
24
+ from PIL import Image
25
+ from scipy import ndimage
26
+ from transformers import (
27
+ CLIPModel, CLIPProcessor, pipeline as hf_pipeline,
28
+ BlipProcessor, BlipForQuestionAnswering,
29
+ SegformerImageProcessor, AutoModelForSemanticSegmentation,
30
+ )
31
+ from diffusers import StableDiffusionInpaintPipeline
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # CONFIG — update these to match your own HF repos
35
+ # ---------------------------------------------------------------------------
36
+ HF_DATASET_REPO = "lihicarmeli/fashion-stylist-multimodal-v2" # your HF dataset repo
37
+ HF_WINNING_MODEL = "openai/clip-vit-base-patch32" # winning embedding model (Part 3)
38
+ EMBEDDINGS_FILE = "final_image_embeddings.npy" # uploaded next to this app.py
39
+ METADATA_FILE = "catalog_metadata.parquet" # uploaded next to this app.py
40
+ SAMPLE_PHOTOS = ["samples/demo_woman.jpg", "samples/demo_man.jpg", "samples/demo_teen.jpg"]
41
+
42
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # LOAD DATA + WINNING EMBEDDING MODEL (runs once, on Space startup)
46
+ # ---------------------------------------------------------------------------
47
+ print("Loading dataset from HF Hub...")
48
+ ds = load_dataset(HF_DATASET_REPO)
49
+ df = ds["train"].to_pandas()
50
+ images = [ds["train"][i]["image_improved"] for i in range(len(ds["train"]))]
51
+
52
+ print("Loading precomputed embeddings...")
53
+ image_embeddings = np.load(EMBEDDINGS_FILE).astype("float32")
54
+
55
+ print("Loading winning embedding model (CLIP) from HF Hub...")
56
+ win_model = CLIPModel.from_pretrained(HF_WINNING_MODEL).to(DEVICE).eval()
57
+ win_processor = CLIPProcessor.from_pretrained(HF_WINNING_MODEL)
58
+
59
+ print("Building FAISS index...")
60
+ dimension = image_embeddings.shape[1]
61
+ faiss_img_index = faiss.IndexFlatL2(dimension)
62
+ faiss.normalize_L2(image_embeddings)
63
+ faiss_img_index.add(image_embeddings)
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # GENERATION MODELS — the 3 "Good Examples" patterns
67
+ # ---------------------------------------------------------------------------
68
+ print("Loading small language model for text generation (Qwen2.5-0.5B-Instruct)...")
69
+ caption_gen_pipe = hf_pipeline(
70
+ "text-generation",
71
+ model="Qwen/Qwen2.5-0.5B-Instruct",
72
+ device=0 if DEVICE == "cuda" else -1,
73
+ )
74
+
75
+ print("Loading small vision-language model for VQA (BLIP-VQA-base)...")
76
+ vqa_processor = BlipProcessor.from_pretrained("Salesforce/blip-vqa-base")
77
+ vqa_model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base").to(DEVICE)
78
+
79
+ print("Loading clothing segmentation model (helper - finds where the clothes are)...")
80
+ seg_processor = SegformerImageProcessor.from_pretrained("mattmdjaga/segformer_b2_clothes")
81
+ seg_model = AutoModelForSemanticSegmentation.from_pretrained("mattmdjaga/segformer_b2_clothes").to(DEVICE)
82
+
83
+ print("Loading image-generation model for the new outfit (Stable Diffusion Inpainting)...")
84
+ inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(
85
+ "runwayml/stable-diffusion-inpainting",
86
+ torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32,
87
+ safety_checker=None,
88
+ ).to(DEVICE)
89
+
90
+ GENDERS = sorted(df["gender"].unique().tolist())
91
+ AGE_GROUPS = sorted(df["age_group"].unique().tolist())
92
+ SKIN_TONES = sorted(df["skin_tone"].unique().tolist())
93
+ UNDERTONES = sorted(df["undertone"].unique().tolist())
94
+ STYLES = sorted(df["style_preference"].unique().tolist())
95
+
96
+ print("Space ready.")
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # EMBEDDING + SEARCH (Part 3 logic, unchanged)
101
+ # ---------------------------------------------------------------------------
102
+ @torch.no_grad()
103
+ def embed_query_image(pil_image):
104
+ inputs = win_processor(images=pil_image, return_tensors="pt").to(DEVICE)
105
+ feats = win_model.get_image_features(**inputs)
106
+ feats = feats.pooler_output if hasattr(feats, "pooler_output") else feats
107
+ return feats.cpu().numpy().astype("float32")
108
+
109
+
110
+ @torch.no_grad()
111
+ def embed_query_text(sentence):
112
+ inputs = win_processor(text=[sentence], return_tensors="pt", padding=True, truncation=True).to(DEVICE)
113
+ feats = win_model.get_text_features(**inputs)
114
+ feats = feats.pooler_output if hasattr(feats, "pooler_output") else feats
115
+ return feats.cpu().numpy().astype("float32")
116
+
117
+
118
+ def build_feature_sentence(skin_tone, undertone, style_preference, gender=None, age_group=None):
119
+ descriptor = " ".join(p for p in [age_group, gender] if p) or "person"
120
+ return (
121
+ f"a {descriptor} with {skin_tone} skin tone and {undertone} undertone, "
122
+ f"wearing a {style_preference} style outfit"
123
+ )
124
+
125
+
126
+ def faiss_filtered_search(query_emb, top_k=3, exclude_idx=None, gender=None, age_group=None):
127
+ faiss.normalize_L2(query_emb)
128
+ k = len(df)
129
+ distances, indices = faiss_img_index.search(query_emb, k)
130
+ distances, indices = distances[0], indices[0]
131
+
132
+ def collect(require_gender, require_age):
133
+ kept_i, kept_d = [], []
134
+ for idx, dist in zip(indices, distances):
135
+ if idx == -1 or (exclude_idx is not None and idx == exclude_idx):
136
+ continue
137
+ row = df.iloc[idx]
138
+ if require_gender and gender and str(row["gender"]).lower() != str(gender).lower():
139
+ continue
140
+ if require_age and age_group and str(row["age_group"]).lower() != str(age_group).lower():
141
+ continue
142
+ kept_i.append(idx)
143
+ kept_d.append(dist)
144
+ if len(kept_i) == top_k:
145
+ break
146
+ return kept_i, kept_d
147
+
148
+ kept_i, kept_d = collect(True, True)
149
+ if len(kept_i) < top_k:
150
+ kept_i, kept_d = collect(True, False)
151
+ if len(kept_i) < top_k:
152
+ kept_i, kept_d = collect(False, False)
153
+
154
+ return np.array(kept_i), df.iloc[kept_i], np.array(kept_d)
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # GENERATION — 3 "Good Examples" patterns (Part 4)
159
+ # ---------------------------------------------------------------------------
160
+
161
+ # --- Pattern 1: small Language Model -> generate text ---
162
+ def generate_stylist_caption(row):
163
+ user_prompt = (
164
+ f"Write one short, warm sentence (max 25 words) from a fashion stylist, recommending this look: "
165
+ f"a {row['style_preference']} style outfit in {row['primary_color']} and {row['secondary_color']}, "
166
+ f"best colors: {row['recommended_colors']}. Be specific and stylish, no hashtags."
167
+ )
168
+ messages = [{"role": "user", "content": user_prompt}]
169
+ output = caption_gen_pipe(messages, max_new_tokens=40, do_sample=True, temperature=0.7)
170
+ return output[0]["generated_text"][-1]["content"].strip()
171
+
172
+
173
+ # --- Pattern 2: small Vision-Language Model -> answer questions about an image ---
174
+ @torch.no_grad()
175
+ def answer_question_about_image(pil_image, question):
176
+ inputs = vqa_processor(pil_image.convert("RGB"), question, return_tensors="pt").to(DEVICE)
177
+ output_ids = vqa_model.generate(**inputs, max_new_tokens=20)
178
+ return vqa_processor.decode(output_ids[0], skip_special_tokens=True)
179
+
180
+
181
+ # --- Pattern 3: small Vision-Language Model -> generate the FULL LOOK (full-body image) ---
182
+ FACE_LABEL_ID = 11
183
+ HAIR_LABEL_ID = 2
184
+
185
+
186
+ @torch.no_grad()
187
+ def get_face_protect_mask(pil_image, use_geometric_fallback=True):
188
+ """Returns a boolean mask (True = protect) covering the face + hair, at pil_image's own
189
+ resolution. Combines the segmentation model's prediction with a fixed geometric ellipse
190
+ (centered, where a face statistically sits in a headshot crop) as a safety net - so even
191
+ if segmentation misclassifies an unusual headwear/turban, the visible face is still
192
+ guaranteed to be protected."""
193
+ inputs = seg_processor(images=pil_image, return_tensors="pt").to(DEVICE)
194
+ logits = seg_model(**inputs).logits
195
+ upsampled = torch.nn.functional.interpolate(
196
+ logits, size=pil_image.size[::-1], mode="bilinear", align_corners=False
197
+ )
198
+ pred_seg = upsampled.argmax(dim=1)[0].cpu().numpy()
199
+ seg_protect = np.isin(pred_seg, [FACE_LABEL_ID, HAIR_LABEL_ID])
200
+
201
+ if not use_geometric_fallback:
202
+ return seg_protect
203
+
204
+ h, w = seg_protect.shape
205
+ yy, xx = np.mgrid[0:h, 0:w]
206
+ cy, cx = h * 0.42, w * 0.5 # face center: slightly above vertical middle of a headshot
207
+ ry, rx = h * 0.30, w * 0.22 # ellipse radii tuned for a tight headshot/bust crop
208
+ geometric_protect = (((xx - cx) / rx) ** 2 + ((yy - cy) / ry) ** 2) <= 1.0
209
+
210
+ return seg_protect | geometric_protect
211
+
212
+
213
+ def build_full_look_prompt(row):
214
+ gender_word = "man" if str(row["gender"]).lower() in ("man", "male") else "woman"
215
+ return (
216
+ f"full body fashion photo of a {row['age_group']} {gender_word}, standing, "
217
+ f"wearing a {row['style_preference']} style outfit: {row['outfit_top']}, "
218
+ f"{row['outfit_bottom']}, {row['outfit_shoes']}, {row['outfit_accessory']}, "
219
+ f"in {row['primary_color']} and {row['secondary_color']}, "
220
+ f"studio lighting, plain background, head to toe, high quality fashion photography"
221
+ )
222
+
223
+
224
+ def generate_new_outfit_image(pil_image, row, target_gender=None, canvas_size=(512, 1024),
225
+ head_width_frac=0.45, steps=40, guidance_scale=8.0):
226
+ """Generates the FULL LOOK: keeps the original face pixel-identical (scaled down to a
227
+ realistic head-to-body proportion) and generates the rest of a standing figure wearing
228
+ the complete recommended outfit (top, bottom, shoes, accessory) around it."""
229
+ if target_gender is not None and str(row["gender"]).lower() != str(target_gender).lower():
230
+ print(f"⚠️ Warning: recommended row gender ({row['gender']}) != expected gender "
231
+ f"({target_gender}) - double-check which matched_rows was passed in.")
232
+
233
+ base = pil_image.convert("RGB")
234
+ protect = get_face_protect_mask(base) # (H, W) bool, at base's own resolution
235
+
236
+ canvas_w, canvas_h = canvas_size
237
+ head_w = int(canvas_w * head_width_frac)
238
+ scale = head_w / base.width
239
+ head_h = int(base.height * scale)
240
+
241
+ resized_face_crop = base.resize((head_w, head_h))
242
+ resized_protect_img = Image.fromarray(protect.astype(np.uint8) * 255).resize(
243
+ (head_w, head_h), resample=Image.NEAREST
244
+ )
245
+
246
+ canvas = Image.new("RGB", canvas_size, color=(128, 128, 128))
247
+ paste_x = (canvas_w - head_w) // 2
248
+ paste_y = int(canvas_h * 0.03)
249
+ canvas.paste(resized_face_crop, (paste_x, paste_y))
250
+
251
+ mask_arr = np.full((canvas_h, canvas_w), 255, dtype=np.uint8) # 255 = let the model generate
252
+ protect_resized_arr = np.array(resized_protect_img) > 127
253
+ mask_arr[paste_y:paste_y + head_h, paste_x:paste_x + head_w][protect_resized_arr] = 0
254
+ mask = Image.fromarray(mask_arr).convert("L")
255
+
256
+ prompt = build_full_look_prompt(row)
257
+ generated = inpaint_pipe(
258
+ prompt=prompt,
259
+ image=canvas,
260
+ mask_image=mask,
261
+ num_inference_steps=steps,
262
+ guidance_scale=guidance_scale,
263
+ height=canvas_h,
264
+ width=canvas_w,
265
+ ).images[0]
266
+
267
+ # Hard-composite: guarantees zero change on the protected face/hair pixels
268
+ final_image = Image.composite(generated, canvas, mask)
269
+ return final_image, prompt
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # SHOP LINKS (Part 3 logic, unchanged)
274
+ # ---------------------------------------------------------------------------
275
+ RETAILER_SEARCH_URLS = {
276
+ "zara": "https://www.zara.com/us/en/search?searchTerm={query}&section={section}",
277
+ "hm": "https://www2.hm.com/en_us/search-results.html?q={query}",
278
+ "asos": "https://www.asos.com/us/{dept}/search/?q={query}",
279
+ "mango": "https://shop.mango.com/us/en/search?kw={query}",
280
+ "shein": "https://us.shein.com/pdsearch/{query}/",
281
+ }
282
+
283
+
284
+ def normalize_gender(gender):
285
+ g = str(gender).strip().lower() if gender is not None else ""
286
+ return "men" if g in ("male", "man", "men", "m") else "women"
287
+
288
+
289
+ def to_shop_link(retailer, value, gender=None):
290
+ dept = normalize_gender(gender)
291
+ if retailer == "zara":
292
+ section = "MAN" if dept == "men" else "WOMAN"
293
+ return RETAILER_SEARCH_URLS["zara"].format(query=urllib.parse.quote(str(value)), section=section)
294
+ if retailer == "asos":
295
+ return RETAILER_SEARCH_URLS["asos"].format(query=urllib.parse.quote(str(value)), dept=dept)
296
+ gender_word = "men's" if dept == "men" else "women's"
297
+ return RETAILER_SEARCH_URLS[retailer].format(query=urllib.parse.quote(f"{gender_word} {value}"))
298
+
299
+
300
+ def shop_links_markdown(row):
301
+ lines = []
302
+ for retailer, col in [("zara", "search_query_zara"), ("hm", "search_query_hm"),
303
+ ("asos", "search_query_asos"), ("mango", "search_query_mango"),
304
+ ("shein", "search_query_shein")]:
305
+ link = to_shop_link(retailer, row[col], gender=row["gender"])
306
+ lines.append(f"- **{retailer.upper()}**: [{row[col]}]({link})")
307
+ return "\n".join(lines)
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # MAIN PIPELINE — shared by both input paths, runs all 3 GenAI patterns
312
+ # ---------------------------------------------------------------------------
313
+ def run_pipeline(matched_indices, matched_rows, matched_scores, base_image=None, question=None,
314
+ expected_gender=None):
315
+ if len(matched_indices) == 0:
316
+ return [], None, "No matches found — try different filters.", "", ""
317
+
318
+ similarity = [round(1.0 - (d / 2.0), 3) for d in matched_scores]
319
+ gallery = [(images[idx], f"Match #{i + 1} ({similarity[i]})") for i, idx in enumerate(matched_indices)]
320
+
321
+ top_row = matched_rows.iloc[0]
322
+ # Pattern 3 needs a base photo: the user's own photo (Path A) or the top catalog match (Path B)
323
+ edit_base_image = base_image if base_image is not None else images[matched_indices[0]]
324
+ # If no explicit gender filter was given, fall back to the matched row's own gender, so
325
+ # the generated outfit's wording always matches the person actually shown in the photo.
326
+ gender_for_generation = expected_gender or top_row["gender"]
327
+
328
+ caption = generate_stylist_caption(top_row) # Pattern 1
329
+ new_image, _prompt = generate_new_outfit_image(
330
+ edit_base_image, top_row, target_gender=gender_for_generation
331
+ ) # Pattern 3
332
+
333
+ answer = ""
334
+ if question:
335
+ answer = answer_question_about_image(edit_base_image, question) # Pattern 2
336
+
337
+ shop_md = "\n\n---\n\n".join(
338
+ f"**Match #{i + 1}** ({row['gender']}, {row['style_preference']}, colors: {row['recommended_colors']})\n"
339
+ + shop_links_markdown(row)
340
+ for i, (_, row) in enumerate(matched_rows.iterrows())
341
+ )
342
+ return gallery, new_image, caption, answer, shop_md
343
+
344
+
345
+ def recommend_from_photo(photo, gender, age_group, question):
346
+ if photo is None:
347
+ return [], None, "Please upload a photo or pick a Quick Starter.", "", ""
348
+ query_emb = embed_query_image(photo)
349
+ idx, rows, scores = faiss_filtered_search(query_emb, gender=gender or None, age_group=age_group or None)
350
+ return run_pipeline(idx, rows, scores, base_image=photo, question=question, expected_gender=gender)
351
+
352
+
353
+ def recommend_from_features(skin_tone, undertone, style, gender, age_group, question):
354
+ sentence = build_feature_sentence(skin_tone, undertone, style, gender, age_group)
355
+ query_emb = embed_query_text(sentence)
356
+ idx, rows, scores = faiss_filtered_search(query_emb, gender=gender or None, age_group=age_group or None)
357
+ return run_pipeline(idx, rows, scores, base_image=None, question=question, expected_gender=gender)
358
+
359
+
360
+ # ---------------------------------------------------------------------------
361
+ # GRADIO UI
362
+ # ---------------------------------------------------------------------------
363
+ with gr.Blocks(title="AI Fashion Stylist") as demo:
364
+ gr.Markdown(
365
+ "# 👗 AI Fashion Stylist\n"
366
+ "Upload a photo **or** describe your style — get 3 real catalog matches, shop links, "
367
+ "a brand-new AI-edited outfit photo, a stylist note, and answers to your styling questions."
368
+ )
369
+
370
+ with gr.Tab("📸 Upload a Photo"):
371
+ with gr.Row():
372
+ photo_in = gr.Image(type="pil", label="Your photo")
373
+ with gr.Column():
374
+ gender_a = gr.Dropdown(GENDERS, label="Gender (optional)")
375
+ age_a = gr.Dropdown(AGE_GROUPS, label="Age group (optional)")
376
+ question_a = gr.Textbox(label="Ask the stylist a question about your photo (optional)",
377
+ placeholder="e.g. What style would suit me best?")
378
+ btn_a = gr.Button("Find My Style", variant="primary")
379
+ gallery_a = gr.Gallery(label="Top 3 Matches", columns=3)
380
+ new_img_a = gr.Image(label="✨ New AI-Edited Outfit")
381
+ caption_a = gr.Textbox(label="Stylist Note")
382
+ answer_a = gr.Textbox(label="Answer to your question")
383
+ shop_a = gr.Markdown(label="Shop the Look")
384
+ btn_a.click(
385
+ recommend_from_photo,
386
+ [photo_in, gender_a, age_a, question_a],
387
+ [gallery_a, new_img_a, caption_a, answer_a, shop_a],
388
+ )
389
+
390
+ gr.Examples(
391
+ examples=[[p, None, None, "What style would suit me best?"] for p in SAMPLE_PHOTOS],
392
+ inputs=[photo_in, gender_a, age_a, question_a],
393
+ outputs=[gallery_a, new_img_a, caption_a, answer_a, shop_a],
394
+ fn=recommend_from_photo,
395
+ cache_examples=True, # precomputed at Space startup -> truly "1-click and see a result"
396
+ label="Quick Starters",
397
+ )
398
+
399
+ with gr.Tab("✏️ Describe Your Style"):
400
+ with gr.Row():
401
+ with gr.Column():
402
+ skin_b = gr.Dropdown(SKIN_TONES, label="Skin tone", value=SKIN_TONES[0])
403
+ undertone_b = gr.Dropdown(UNDERTONES, label="Undertone", value=UNDERTONES[0])
404
+ style_b = gr.Dropdown(STYLES, label="Style preference", value=STYLES[0])
405
+ gender_b = gr.Dropdown(GENDERS, label="Gender", value=GENDERS[0])
406
+ age_b = gr.Dropdown(AGE_GROUPS, label="Age group", value=AGE_GROUPS[0])
407
+ question_b = gr.Textbox(label="Ask the stylist a question about the top match (optional)",
408
+ placeholder="e.g. Is this outfit formal or casual?")
409
+ btn_b = gr.Button("Find My Style", variant="primary")
410
+ gallery_b = gr.Gallery(label="Top 3 Matches", columns=3)
411
+ new_img_b = gr.Image(label="✨ New AI-Edited Outfit")
412
+ caption_b = gr.Textbox(label="Stylist Note")
413
+ answer_b = gr.Textbox(label="Answer to your question")
414
+ shop_b = gr.Markdown(label="Shop the Look")
415
+ btn_b.click(
416
+ recommend_from_features,
417
+ [skin_b, undertone_b, style_b, gender_b, age_b, question_b],
418
+ [gallery_b, new_img_b, caption_b, answer_b, shop_b],
419
+ )
420
+
421
+ gr.Examples(
422
+ examples=[
423
+ ["deep", "cool", "boho", "woman", "young adult", "Is this outfit formal or casual?"],
424
+ ["tan", "warm", "elegant", "man", "adult", "What occasion suits this outfit?"],
425
+ ["fair", "neutral", "minimalist", "woman", "teen", "What season is this outfit best for?"],
426
+ ],
427
+ inputs=[skin_b, undertone_b, style_b, gender_b, age_b, question_b],
428
+ outputs=[gallery_b, new_img_b, caption_b, answer_b, shop_b],
429
+ fn=recommend_from_features,
430
+ cache_examples=True, # precomputed at Space startup -> truly "1-click and see a result"
431
+ label="Quick Starters",
432
+ )
433
+
434
+ if __name__ == "__main__":
435
+ demo.launch()