Anusha806 commited on
Commit
c535089
·
1 Parent(s): 590858e
Files changed (2) hide show
  1. app.py +472 -127
  2. requirements.txt +4 -2
app.py CHANGED
@@ -486,13 +486,14 @@ def hybrid_scale(dense, sparse, alpha: float):
486
  return hdense, hsparse
487
 
488
  def extract_intent_from_openai(query: str):
489
- prompt = f'''
490
  You are an assistant for a fashion search engine. Extract the user's intent from the following query.
491
  Return a Python dictionary with keys: category, gender, subcategory, color.
492
  If something is missing, use null.
 
493
  Query: "{query}"
494
  Only return the dictionary.
495
- '''
496
  try:
497
  response = openai.ChatCompletion.create(
498
  model="gpt-4",
@@ -504,26 +505,141 @@ Only return the dictionary.
504
  return structured
505
  except Exception as e:
506
  print(f"⚠️ OpenAI intent extraction failed: {e}")
507
- return {}
508
-
509
- def is_duplicate(img, seen_hashes):
510
- h = hash(img.tobytes())
511
- if h in seen_hashes:
512
- return True
513
- seen_hashes.add(h)
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  return False
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  # ------------------- Search Functions -------------------
517
  def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gender_override: str = None):
518
  intent = extract_intent_from_openai(query)
519
- gender = intent.get("gender")
520
- category = intent.get("category")
521
- subcategory = intent.get("subcategory")
522
- color = intent.get("color")
 
 
 
 
 
 
523
  if gender_override:
524
  gender = gender_override
525
 
 
526
  filter = {}
 
 
527
  if gender:
528
  filter["gender"] = gender
529
  if category:
@@ -533,9 +649,42 @@ def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gend
533
  filter["articleType"] = category
534
  if subcategory:
535
  filter["subCategory"] = subcategory
 
 
 
 
 
 
 
 
 
 
 
536
  if color:
537
  filter["baseColour"] = color
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  sparse = bm25.encode_queries(query)
540
  dense = model.encode(query).tolist()
541
  hdense, hsparse = hybrid_scale(dense, sparse, alpha=alpha)
@@ -545,16 +694,25 @@ def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gend
545
  vector=hdense,
546
  sparse_vector=hsparse,
547
  include_metadata=True,
548
- filter=filter if filter else None
549
  )
550
 
 
551
  if len(result["matches"]) == 0:
552
  print("⚠️ No results, retrying with alpha=0 sparse only")
553
  hdense, hsparse = hybrid_scale(dense, sparse, alpha=0)
554
- result = index.query(top_k=100, vector=hdense, sparse_vector=hsparse, include_metadata=True, filter=filter)
 
 
 
 
 
 
555
 
 
556
  imgs_with_captions = []
557
  seen_hashes = set()
 
558
  for r in result["matches"]:
559
  idx = int(r["id"])
560
  img = images[idx]
@@ -563,179 +721,350 @@ def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gend
563
  img = Image.fromarray(np.array(img))
564
  padded = ImageOps.pad(img, (256, 256), color="white")
565
  caption = str(meta.get("productDisplayName", "Unknown Product"))
 
566
  if not is_duplicate(padded, seen_hashes):
567
  imgs_with_captions.append((padded, caption))
 
568
  if len(imgs_with_captions) >= end:
569
  break
570
 
571
  return imgs_with_captions
572
 
573
  def search_by_image(uploaded_image, alpha=0.5, start=0, end=12):
 
574
  processed = clip_processor(images=uploaded_image, return_tensors="pt").to(device)
 
575
  with torch.no_grad():
576
  image_vec = clip_model.get_image_features(**processed)
577
  image_vec = image_vec.cpu().numpy().flatten().tolist()
578
 
579
- result = index.query(top_k=100, vector=image_vec, include_metadata=True)
 
 
 
 
 
 
 
580
  imgs_with_captions = []
581
  seen_hashes = set()
582
 
583
- for r in result["matches"]:
 
584
  idx = int(r["id"])
585
  img = images[idx]
586
  meta = r.get("metadata", {})
587
  caption = str(meta.get("productDisplayName", "Unknown Product"))
 
588
  if not isinstance(img, Image.Image):
589
  img = Image.fromarray(np.array(img))
 
590
  padded = ImageOps.pad(img, (256, 256), color="white")
 
591
  if not is_duplicate(padded, seen_hashes):
592
  imgs_with_captions.append((padded, caption))
 
593
  if len(imgs_with_captions) >= end:
594
  break
595
 
596
  return imgs_with_captions
597
 
598
- # ------------------- UI -------------------
599
- # custom_css = """
600
- # .search-btn { width: 100%; }
601
- # .gr-row { gap: 8px !important; }
602
- # .query-slider > div { margin-bottom: 4px !important; }
603
- # .gr-gallery-item { width: 256px !important; height: 256px !important; }
604
- # .gr-gallery-item img { width: 100% !important; height: 100% !important; object-fit: cover !important; }
605
- # """
606
 
607
- # with gr.Blocks(css=custom_css) as demo:
608
- # gr.Markdown("# 🛍️ Fashion Product Hybrid Search (with GPT-4 powered query parsing)")
609
 
610
- # with gr.Row(equal_height=True):
611
- # with gr.Column(scale=5, elem_classes="query-slider"):
612
- # query = gr.Textbox(label="Enter your fashion search query", placeholder="e.g., black sneakers for women")
613
- # alpha = gr.Slider(0, 1, value=0.5, label="Hybrid Weight (alpha: 0=sparse, 1=dense)")
614
- # gender_dropdown = gr.Dropdown(["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender Filter (optional)")
615
- # with gr.Column(scale=1):
616
- # image_input = gr.Image(type="pil", label="Upload an image (optional)", sources=["upload", "clipboard"], height=256, width=356)
617
 
618
- # search_btn = gr.Button("Search", elem_classes="search-btn")
619
- # gallery = gr.Gallery(label="Search Results", columns=6, height=None)
620
- # load_more_btn = gr.Button("Load More")
 
 
 
 
 
 
 
621
 
622
- # search_offset = gr.State(0)
623
- # current_query = gr.State("")
624
- # current_image = gr.State(None)
625
- # current_gender = gr.State("")
626
- # shown_results = gr.State([])
627
- # shown_ids = gr.State(set())
628
 
629
- # def unified_search(q, uploaded_image, a, offset, gender_ui):
630
- # start = 0
631
- # end = 12
632
- # filters = extract_intent_from_openai(q) if q.strip() else {}
633
- # gender_override = gender_ui if gender_ui else filters.get("gender")
 
 
 
 
 
 
634
 
635
- # if uploaded_image is not None:
636
- # results = search_by_image(uploaded_image, a, start, end)
637
- # elif q.strip():
638
- # results = search_fashion(q, a, start, end, gender_override)
639
- # else:
640
- # results = []
641
 
642
- # seen_ids = {r[1] for r in results}
643
- # return results, end, q, uploaded_image, gender_override, results, seen_ids
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
 
645
- # search_btn.click(unified_search, inputs=[query, image_input, alpha, search_offset, gender_dropdown],
646
- # outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids])
 
 
 
 
 
 
647
 
648
- # def load_more_fn(a, offset, q, img, gender_ui, prev_results, prev_ids):
649
- # start = offset
650
- # end = offset + 12
651
- # gender_override = gender_ui
652
 
653
- # if img is not None:
654
- # new_results = search_by_image(img, a, start, end)
655
- # elif q.strip():
656
- # new_results = search_fashion(q, a, start, end, gender_override)
657
- # else:
658
- # new_results = []
659
 
660
- # filtered_new = []
661
- # new_ids = set()
662
- # for item in new_results:
663
- # img_obj, caption = item
664
- # if caption not in prev_ids:
665
- # filtered_new.append(item)
666
- # new_ids.add(caption)
667
 
668
- # combined = prev_results + filtered_new
669
- # updated_ids = prev_ids.union(new_ids)
670
 
671
- # return combined, end, combined, updated_ids
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
- # load_more_btn.click(load_more_fn, inputs=[alpha, search_offset, current_query, current_image, current_gender, shown_results, shown_ids],
674
- # outputs=[gallery, search_offset, shown_results, shown_ids])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
 
676
- # gr.Markdown("🧠 Powered by OpenAI + Hybrid AI Fashion Search")
 
 
 
677
 
678
- # demo.launch()
679
- import gradio as gr
 
 
 
 
680
 
681
- custom_css = """
682
- /* Container */
683
  .gr-gallery {
684
- display: flex !important;
685
- flex-wrap: wrap;
686
- gap: 10px;
687
- justify-content: center;
688
  }
689
-
690
- /* Each item */
691
  .gr-gallery-item {
692
- flex: 0 0 calc(16.66% - 10px); /* 6 columns on desktop */
693
- max-width: calc(16.66% - 10px);
694
- height: 256px !important;
 
695
  overflow: hidden;
696
  }
697
-
698
- /* Image inside each item */
 
 
699
  .gr-gallery-item img {
 
700
  width: 100% !important;
701
  height: 100% !important;
702
- object-fit: cover !important;
703
  }
704
 
705
- /* For mobile: 3 columns */
706
- @media (max-width: 768px) {
707
- .gr-gallery-item {
708
- flex: 0 0 calc(33.33% - 10px); /* 3 columns on mobile */
709
- max-width: calc(33.33% - 10px);
710
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  """
713
 
714
- with gr.Blocks(css=custom_css) as demo:
715
- gr.Markdown("## 🛍️ Responsive Fashion Product Search")
716
-
717
- with gr.Row():
718
- with gr.Column(scale=5, elem_classes="query-slider"):
719
- query = gr.Textbox(label="Search", placeholder="e.g. black dress for women")
720
- alpha = gr.Slider(0, 1, value=0.5, label="Hybrid Weight")
721
- gender_dropdown = gr.Dropdown(["", "Men", "Women", "Unisex"], label="Gender (optional)")
722
- with gr.Column(scale=1):
723
- image_input = gr.Image(type="pil", label="Upload Image", sources=["upload", "clipboard"], height=256)
724
-
725
- search_btn = gr.Button("Search", elem_classes="search-btn")
726
- gallery = gr.Gallery(label="Results", columns=6, height=None, allow_preview=True)
727
- load_more_btn = gr.Button("Load More")
728
-
729
- search_offset = gr.State(0)
730
- current_query = gr.State("")
731
- current_image = gr.State(None)
732
- current_gender = gr.State("")
733
- shown_results = gr.State([])
734
- shown_ids = gr.State(set())
735
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  def unified_search(q, uploaded_image, a, offset, gender_ui):
737
  start = 0
738
  end = 12
 
739
  filters = extract_intent_from_openai(q) if q.strip() else {}
740
  gender_override = gender_ui if gender_ui else filters.get("gender")
741
 
@@ -749,12 +1078,28 @@ with gr.Blocks(css=custom_css) as demo:
749
  seen_ids = {r[1] for r in results}
750
  return results, end, q, uploaded_image, gender_override, results, seen_ids
751
 
752
- search_btn.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  unified_search,
754
- inputs=[query, image_input, alpha, search_offset, gender_dropdown],
755
  outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids]
756
  )
757
 
 
758
  def load_more_fn(a, offset, q, img, gender_ui, prev_results, prev_ids):
759
  start = offset
760
  end = offset + 12
@@ -786,7 +1131,7 @@ with gr.Blocks(css=custom_css) as demo:
786
  outputs=[gallery, search_offset, shown_results, shown_ids]
787
  )
788
 
789
- gr.Markdown("🧠 Powered by OpenAI + Hybrid AI Fashion Search")
790
 
791
  demo.launch()
792
 
 
486
  return hdense, hsparse
487
 
488
  def extract_intent_from_openai(query: str):
489
+ prompt = f"""
490
  You are an assistant for a fashion search engine. Extract the user's intent from the following query.
491
  Return a Python dictionary with keys: category, gender, subcategory, color.
492
  If something is missing, use null.
493
+
494
  Query: "{query}"
495
  Only return the dictionary.
496
+ """
497
  try:
498
  response = openai.ChatCompletion.create(
499
  model="gpt-4",
 
505
  return structured
506
  except Exception as e:
507
  print(f"⚠️ OpenAI intent extraction failed: {e}")
508
+ return {"include": {}, "exclude": {}}
509
+ #-----------------below changed------------------------------#
510
+
511
+ import imagehash
512
+ from PIL import Image
513
+
514
+ def is_duplicate(img, existing_hashes, hash_size=16, tolerance=0):
515
+ """
516
+ Checks if the image is a near-duplicate based on perceptual hash.
517
+ :param img: PIL Image
518
+ :param existing_hashes: set of previously seen hashes
519
+ :param hash_size: size of the hash (default=16 for more precision)
520
+ :param tolerance: allowable Hamming distance for near-duplicates
521
+ :return: (bool) whether image is duplicate
522
+ """
523
+ img_hash = imagehash.phash(img, hash_size=hash_size)
524
+ for h in existing_hashes:
525
+ if abs(img_hash - h) <= tolerance:
526
+ return True
527
+ existing_hashes.add(img_hash)
528
  return False
529
 
530
+ def extract_metadata_filters(query: str):
531
+ query_lower = query.lower()
532
+ gender = None
533
+ category = None
534
+ subcategory = None
535
+ color = None
536
+
537
+ # --- Gender Mapping ---
538
+ gender_map = {
539
+ "men": "Men", "man": "Men", "mens": "Men", "mans": "Men", "male": "Men",
540
+ "women": "Women", "woman": "Women", "womens": "Women", "female": "Women",
541
+ "boys": "Boys", "boy": "Boys",
542
+ "girls": "Girls", "girl": "Girls",
543
+ "kids": "Kids", "kid": "Kids",
544
+ "unisex": "Unisex"
545
+ }
546
+ for term, mapped_value in gender_map.items():
547
+ if term in query_lower:
548
+ gender = mapped_value
549
+ break
550
+
551
+ # --- Category Mapping ---
552
+ category_map = {
553
+ "shirt": "Shirts",
554
+ "tshirt": "Tshirts",
555
+ "t-shirt": "Tshirts",
556
+ "jeans": "Jeans",
557
+ "watch": "Watches",
558
+ "kurta": "Kurtas",
559
+ "dress": "Dresses",
560
+ "trousers": "Trousers", "pants": "Trousers",
561
+ "shorts": "Shorts",
562
+ "footwear": "Footwear",
563
+ "shoes": "Shoes",
564
+ "fashion": "Apparel"
565
+ }
566
+ for term, mapped_value in category_map.items():
567
+ if term in query_lower:
568
+ category = mapped_value
569
+ break
570
+
571
+ # --- SubCategory Mapping ---
572
+ subCategory_list = [
573
+ "Accessories", "Apparel Set", "Bags", "Bath and Body", "Beauty Accessories",
574
+ "Belts", "Bottomwear", "Cufflinks", "Dress", "Eyes", "Eyewear", "Flip Flops",
575
+ "Fragrance", "Free Gifts", "Gloves", "Hair", "Headwear", "Home Furnishing",
576
+ "Innerwear", "Jewellery", "Lips", "Loungewear and Nightwear", "Makeup",
577
+ "Mufflers", "Nails", "Perfumes", "Sandal", "Saree", "Scarves", "Shoe Accessories",
578
+ "Shoes", "Skin", "Skin Care", "Socks", "Sports Accessories", "Sports Equipment",
579
+ "Stoles", "Ties", "Topwear", "Umbrellas", "Vouchers", "Wallets", "Watches",
580
+ "Water Bottle", "Wristbands"
581
+ ]
582
+ if "topwear" in query_lower or "top" in query_lower:
583
+ subcategory = "Topwear"
584
+ else:
585
+ query_words = query_lower.split()
586
+ for subcat in subCategory_list:
587
+ if subcat.lower() in query_words:
588
+ subcategory = subcat
589
+ break
590
+
591
+ # --- Color Extraction ---
592
+ color_list = [
593
+ "red", "blue", "green", "yellow", "black", "white",
594
+ "orange", "pink", "purple", "brown", "grey", "beige"
595
+ ]
596
+ for c in color_list:
597
+ if c in query_lower:
598
+ color = c.capitalize()
599
+ break
600
+
601
+ # --- Invalid pairs ---
602
+ invalid_pairs = {
603
+ ("Men", "Dresses"), ("Men", "Sarees"), ("Men", "Skirts"),
604
+ ("Boys", "Dresses"), ("Boys", "Sarees"),
605
+ ("Girls", "Boxers"), ("Men", "Heels")
606
+ }
607
+ if (gender, category) in invalid_pairs:
608
+ print(f"⚠️ Invalid pair: {gender} + {category}, dropping gender")
609
+ gender = None
610
+
611
+ # --- Fallback for missing category ---
612
+ if gender and not category:
613
+ category = "Apparel"
614
+
615
+ # --- Refine subcategory for party/wedding-related queries ---
616
+ if "party" in query_lower or "wedding" in query_lower or "cocktail" in query_lower:
617
+ if subcategory in ["Loungewear and Nightwear", "Nightdress", "Innerwear"]:
618
+ subcategory = None # reset it to avoid filtering into wrong items
619
+
620
+
621
+ return gender, category, subcategory, color
622
+
623
  # ------------------- Search Functions -------------------
624
  def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gender_override: str = None):
625
  intent = extract_intent_from_openai(query)
626
+
627
+ include = intent.get("include", {})
628
+ exclude = intent.get("exclude", {})
629
+
630
+ gender = include.get("gender")
631
+ category = include.get("category")
632
+ subcategory = include.get("subcategory")
633
+ color = include.get("color")
634
+
635
+ # Apply override from dropdown
636
  if gender_override:
637
  gender = gender_override
638
 
639
+ # Build Pinecone filter
640
  filter = {}
641
+
642
+ # Inclusion filters
643
  if gender:
644
  filter["gender"] = gender
645
  if category:
 
649
  filter["articleType"] = category
650
  if subcategory:
651
  filter["subCategory"] = subcategory
652
+
653
+ # Step 4: Exclude irrelevant items for party-like queries
654
+ query_lower = query.lower()
655
+ if any(word in query_lower for word in ["party", "wedding", "cocktail", "traditional", "reception"]):
656
+ filter.setdefault("subCategory", {})
657
+ if isinstance(filter["subCategory"], dict):
658
+ filter["subCategory"]["$nin"] = [
659
+ "Loungewear and Nightwear", "Nightdress", "Innerwear", "Sleepwear", "Vests", "Boxers"
660
+ ]
661
+
662
+
663
  if color:
664
  filter["baseColour"] = color
665
 
666
+ # Exclusion filters
667
+ exclude_filter = {}
668
+ if exclude.get("color"):
669
+ exclude_filter["baseColour"] = {"$ne": exclude["color"]}
670
+ if exclude.get("subcategory"):
671
+ exclude_filter["subCategory"] = {"$ne": exclude["subcategory"]}
672
+ if exclude.get("category"):
673
+ exclude_filter["articleType"] = {"$ne": exclude["category"]}
674
+
675
+ # Combine all filters
676
+ if filter and exclude_filter:
677
+ final_filter = {"$and": [filter, exclude_filter]}
678
+ elif filter:
679
+ final_filter = filter
680
+ elif exclude_filter:
681
+ final_filter = exclude_filter
682
+ else:
683
+ final_filter = None
684
+
685
+ print(f"🔍 Using filter: {final_filter} (showing {start} to {end})")
686
+
687
+ # Hybrid encoding
688
  sparse = bm25.encode_queries(query)
689
  dense = model.encode(query).tolist()
690
  hdense, hsparse = hybrid_scale(dense, sparse, alpha=alpha)
 
694
  vector=hdense,
695
  sparse_vector=hsparse,
696
  include_metadata=True,
697
+ filter=final_filter
698
  )
699
 
700
+ # Retry fallback
701
  if len(result["matches"]) == 0:
702
  print("⚠️ No results, retrying with alpha=0 sparse only")
703
  hdense, hsparse = hybrid_scale(dense, sparse, alpha=0)
704
+ result = index.query(
705
+ top_k=100,
706
+ vector=hdense,
707
+ sparse_vector=hsparse,
708
+ include_metadata=True,
709
+ filter=final_filter
710
+ )
711
 
712
+ # Format results
713
  imgs_with_captions = []
714
  seen_hashes = set()
715
+
716
  for r in result["matches"]:
717
  idx = int(r["id"])
718
  img = images[idx]
 
721
  img = Image.fromarray(np.array(img))
722
  padded = ImageOps.pad(img, (256, 256), color="white")
723
  caption = str(meta.get("productDisplayName", "Unknown Product"))
724
+
725
  if not is_duplicate(padded, seen_hashes):
726
  imgs_with_captions.append((padded, caption))
727
+
728
  if len(imgs_with_captions) >= end:
729
  break
730
 
731
  return imgs_with_captions
732
 
733
  def search_by_image(uploaded_image, alpha=0.5, start=0, end=12):
734
+ # Step 1: Preprocess image for CLIP model
735
  processed = clip_processor(images=uploaded_image, return_tensors="pt").to(device)
736
+
737
  with torch.no_grad():
738
  image_vec = clip_model.get_image_features(**processed)
739
  image_vec = image_vec.cpu().numpy().flatten().tolist()
740
 
741
+ # Step 2: Query Pinecone index for similar images
742
+ result = index.query(
743
+ top_k=100, # fetch more to allow deduplication
744
+ vector=image_vec,
745
+ include_metadata=True
746
+ )
747
+
748
+ matches = result["matches"]
749
  imgs_with_captions = []
750
  seen_hashes = set()
751
 
752
+ # Step 3: Deduplicate based on image hash
753
+ for r in matches:
754
  idx = int(r["id"])
755
  img = images[idx]
756
  meta = r.get("metadata", {})
757
  caption = str(meta.get("productDisplayName", "Unknown Product"))
758
+
759
  if not isinstance(img, Image.Image):
760
  img = Image.fromarray(np.array(img))
761
+
762
  padded = ImageOps.pad(img, (256, 256), color="white")
763
+
764
  if not is_duplicate(padded, seen_hashes):
765
  imgs_with_captions.append((padded, caption))
766
+
767
  if len(imgs_with_captions) >= end:
768
  break
769
 
770
  return imgs_with_captions
771
 
 
 
 
 
 
 
 
 
772
 
773
+ import gradio as gr
774
+ import whisper
775
 
776
+ asr_model = whisper.load_model("base")
 
 
 
 
 
 
777
 
778
+ def handle_voice_search(vf_path, a, offset, gender_ui):
779
+ try:
780
+ transcription = asr_model.transcribe(vf_path)["text"].strip()
781
+ except:
782
+ transcription = ""
783
+ filters = extract_intent_from_openai(transcription) if transcription else {}
784
+ gender_override = gender_ui if gender_ui else filters.get("gender")
785
+ results = search_fashion(transcription, a, 0, 12, gender_override)
786
+ seen_ids = {r[1] for r in results}
787
+ return results, 12, transcription, None, gender_override, results, seen_ids
788
 
789
+ custom_css = """
790
+ /* === Global Styling === */
791
+ /* === Override Gradio default background === */
 
 
 
792
 
793
+ html, body {
794
+ height: 100% !important;
795
+ margin: 0 !important;
796
+ padding: 0 !important;
797
+ background: radial-gradient(circle at center, #0b1f36 0%, #033e3e 100%) !important;
798
+ background-attachment: fixed;
799
+ }
800
+
801
+ .gr-root, .gr-block {
802
+ background: transparent !important;
803
+ }
804
 
 
 
 
 
 
 
805
 
806
+ body::before {
807
+ content: "";
808
+ position: fixed;
809
+ top: 0; left: 0;
810
+ width: 100%; height: 100%;
811
+ background: radial-gradient(circle at center, rgba(0, 255, 255, 0.08), transparent);
812
+ z-index: -1;
813
+ }
814
+ #app-bg {
815
+ min-height: 100vh;
816
+ padding: 0;
817
+ margin: 0;
818
+ background: radial-gradient(circle at center, #0b1f36 0%, #033e3e 100%);
819
+ display: flex;
820
+ justify-content: center;
821
+ align-items: flex-start;
822
+ background-attachment: fixed;
823
+ position: relative;
824
+ overflow: hidden;
825
+ }
826
 
827
+ #app-bg::before {
828
+ content: "";
829
+ position: absolute;
830
+ top: 0; left: 0;
831
+ width: 100%; height: 100%;
832
+ background: radial-gradient(circle at center, rgba(0, 255, 255, 0.08), transparent);
833
+ z-index: 0;
834
+ }
835
 
836
+ #main-container {
837
+ z-index: 1;
838
+ position: relative;
839
+ }
840
 
 
 
 
 
 
 
841
 
 
 
 
 
 
 
 
842
 
 
 
843
 
844
+ /* === Heading Style === */
845
+ h1, .gr-markdown h1 {
846
+ font-size: 2.2rem !important;
847
+ font-weight: bold;
848
+ color: #000000;
849
+ text-align: center;
850
+ margin-bottom: 1rem;
851
+ }
852
+
853
+ /* === Tabs === */
854
+ .gr-tab {
855
+ border-radius: 12px !important;
856
+ background-color: #ffffff !important;
857
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
858
+ padding: 16px !important;
859
+ margin-top: 12px;
860
+ }
861
+
862
+ /* === Textbox, Dropdown, Slider === */
863
+ input[type="text"], .gr-textbox textarea, .gr-dropdown, .gr-slider {
864
+ border-radius: 8px !important;
865
+ border: 1px solid #ccc !important;
866
+ padding: 10px !important;
867
+ font-size: 16px;
868
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
869
+ }
870
+
871
+ /* === Image Upload === */
872
+ .gr-image {
873
+ width: 100% !important;
874
+ max-width: 100% !important;
875
+ border-radius: 12px;
876
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
877
+ }
878
 
879
+ /* === Buttons (custom style .button-36) === */
880
+ .gr-button {
881
+ background-color: #DBDBDB !important;
882
+ background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
883
+ border-radius: 8px !important;
884
+ border-style: none !important;
885
+ box-sizing: border-box;
886
+ color: #FFFFFF !important;
887
+ cursor: pointer;
888
+ flex-shrink: 0;
889
+ font-family: "Inter UI","SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
890
+ font-size: 16px;
891
+ font-weight: 500;
892
+ height: 4rem;
893
+ padding: 0 1.6rem;
894
+ text-align: center;
895
+ text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
896
+ transition: all .5s;
897
+ user-select: none;
898
+ -webkit-user-select: none;
899
+ touch-action: manipulation;
900
+ }
901
 
902
+ .gr-button:hover {
903
+ box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px;
904
+ transition-duration: .1s;
905
+ }
906
 
907
+ /* === Responsive padding === */
908
+ @media (min-width: 768px) {
909
+ .gr-button {
910
+ padding: 0 2.6rem;
911
+ }
912
+ }
913
 
914
+ /* === Gallery Grid === */
 
915
  .gr-gallery {
916
+ padding-top: 12px;
 
 
 
917
  }
 
 
918
  .gr-gallery-item {
919
+ width: 128px !important;
920
+ height: 128px !important;
921
+ transition: transform 0.3s ease-in-out;
922
+ border-radius: 8px;
923
  overflow: hidden;
924
  }
925
+ .gr-gallery-item:hover {
926
+ transform: scale(1.06);
927
+ box-shadow: 0 3px 12px rgba(0,0,0,0.15);
928
+ }
929
  .gr-gallery-item img {
930
+ object-fit: cover !important;
931
  width: 100% !important;
932
  height: 100% !important;
933
+ border-radius: 8px;
934
  }
935
 
936
+ /* === Audio Upload === */
937
+ .gr-audio {
938
+ width: 100% !important;
939
+ border-radius: 12px;
940
+ background-color: #fff !important;
941
+ box-shadow: 0 1px 5px rgba(0,0,0,0.1);
942
+ }
943
+
944
+ /* === Footer === */
945
+ .gr-markdown:last-child {
946
+ text-align: center;
947
+ font-size: 14px;
948
+ color: #666;
949
+ padding-top: 1rem;
950
+ }
951
+
952
+ /* === Main Container Centered and Wide === */
953
+ #main-container {
954
+ max-width: 90%;
955
+ width: 1100px;
956
+ margin: 40px auto !important;
957
+ padding: 24px;
958
+ background: #ffffff;
959
+ border-radius: 18px;
960
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
961
+ border: 3px solid orange; /* Orange border */
962
+ }
963
+
964
+
965
+
966
+ /* === Tab Label Styling === */
967
+ button[role="tab"] {
968
+ color: #000000 !important; /* Default tab text color: black */
969
+ font-weight: 500;
970
+ transition: color 0.3s ease-in-out;
971
+ font-size: 16px;
972
+ }
973
+
974
+ /* Active tab title */
975
+ button[role="tab"][aria-selected="true"] {
976
+ color: #f57c00 !important; /* Active tab text color: orange */
977
+ font-weight: bold !important;
978
+ }
979
+
980
+ /* Hover effect on tab titles */
981
+ button[role="tab"]:hover {
982
+ color: #f57c00 !important; /* Orange on hover */
983
+ font-weight: 600;
984
+ cursor: pointer;
985
  }
986
+ /* === Uniform Input Sizes for Text, Audio, Image === */
987
+ .gr-textbox, .gr-audio, .gr-image {
988
+ max-width: 100% !important;
989
+ width: 100% !important;
990
+ }
991
+
992
+ .gr-audio, .gr-image {
993
+ max-width: 500px !important;
994
+ margin: 0 auto;
995
+ }
996
+
997
+ .gr-image {
998
+ height: 256px !important;
999
+ }
1000
+
1001
  """
1002
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
 
1004
+
1005
+
1006
+ with gr.Blocks(css=custom_css) as demo:
1007
+ with gr.Column(elem_id="app-bg"):
1008
+ with gr.Column(elem_id="main-container"):
1009
+ gr.Markdown("# 🛍️ Fashion Product Hybrid Search")
1010
+
1011
+ alpha = gr.Slider(0, 1, value=0.5, label="Hybrid Weight (alpha: 0=sparse, 1=dense)")
1012
+
1013
+ with gr.Tabs():
1014
+ with gr.Tab("Text Search"):
1015
+ query = gr.Textbox(
1016
+ label="Text Query",
1017
+ placeholder="e.g., floral summer dress for women"
1018
+ )
1019
+ gender_dropdown = gr.Dropdown(
1020
+ ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"],
1021
+ label="Gender Filter (optional)"
1022
+ )
1023
+ text_search_btn = gr.Button("Search by Text", elem_classes="search-btn")
1024
+ with gr.Tab("🎙️ Voice Search"):
1025
+ voice_input = gr.Audio(label="Speak Your Query", type="filepath")
1026
+ voice_gender_dropdown = gr.Dropdown(["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender")
1027
+ voice_search_btn = gr.Button("Search by Voice")
1028
+
1029
+
1030
+ with gr.Tab("Image Search"):
1031
+ # image_input = gr.Image(
1032
+ # type="pil",
1033
+ # label="Upload an image",
1034
+ # sources=["upload", "clipboard"],
1035
+ # height=256,
1036
+ # width=356
1037
+ # )
1038
+ image_input = gr.Image(
1039
+ type="pil",
1040
+ label="Upload an image",
1041
+ sources=["upload", "clipboard"],
1042
+ # tool=None,
1043
+ height=400
1044
+ )
1045
+
1046
+ image_gender_dropdown = gr.Dropdown(
1047
+ ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"],
1048
+ label="Gender Filter (optional)"
1049
+ )
1050
+ image_search_btn = gr.Button("Search by Image", elem_classes="search-btn")
1051
+
1052
+ gallery = gr.Gallery(label="Search Results", columns=6, height=None)
1053
+ load_more_btn = gr.Button("Load More")
1054
+
1055
+ # --- UI State Holders ---
1056
+ search_offset = gr.State(0)
1057
+ current_query = gr.State("")
1058
+ current_image = gr.State(None)
1059
+ current_gender = gr.State("")
1060
+ shown_results = gr.State([])
1061
+ shown_ids = gr.State(set())
1062
+
1063
+ # --- Unified Search Function ---
1064
  def unified_search(q, uploaded_image, a, offset, gender_ui):
1065
  start = 0
1066
  end = 12
1067
+
1068
  filters = extract_intent_from_openai(q) if q.strip() else {}
1069
  gender_override = gender_ui if gender_ui else filters.get("gender")
1070
 
 
1078
  seen_ids = {r[1] for r in results}
1079
  return results, end, q, uploaded_image, gender_override, results, seen_ids
1080
 
1081
+ # Text Search
1082
+ # Text Search
1083
+ text_search_btn.click(
1084
+ unified_search,
1085
+ inputs=[query, gr.State(None), alpha, search_offset, gender_dropdown],
1086
+ outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids]
1087
+ )
1088
+
1089
+ voice_search_btn.click(
1090
+ handle_voice_search,
1091
+ inputs=[voice_input, alpha, search_offset, voice_gender_dropdown],
1092
+ outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids]
1093
+ )
1094
+
1095
+ # Image Search
1096
+ image_search_btn.click(
1097
  unified_search,
1098
+ inputs=[gr.State(""), image_input, alpha, search_offset, image_gender_dropdown],
1099
  outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids]
1100
  )
1101
 
1102
+ # --- Load More Button ---
1103
  def load_more_fn(a, offset, q, img, gender_ui, prev_results, prev_ids):
1104
  start = offset
1105
  end = offset + 12
 
1131
  outputs=[gallery, search_offset, shown_results, shown_ids]
1132
  )
1133
 
1134
+ # gr.Markdown("🧠 Powered by OpenAI + Hybrid AI Fashion Search")
1135
 
1136
  demo.launch()
1137
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio
2
  openai
3
  sentence-transformers==2.6.1
4
  torch>=2.0.0
@@ -6,7 +6,9 @@ transformers==4.41.1
6
  datasets
7
  Pillow
8
  pinecone-client==3.2.2
 
9
  scikit-learn
10
  tqdm
11
  numpy
12
- pinecone-text
 
 
1
+ gradio>=4.0.0
2
  openai
3
  sentence-transformers==2.6.1
4
  torch>=2.0.0
 
6
  datasets
7
  Pillow
8
  pinecone-client==3.2.2
9
+ pinecone-text
10
  scikit-learn
11
  tqdm
12
  numpy
13
+ imagehash
14
+ whisper