# app.py import os import time import torch import numpy as np import gradio as gr from PIL import Image, ImageOps from tqdm.auto import tqdm from datasets import load_dataset from sentence_transformers import SentenceTransformer from pinecone import Pinecone, ServerlessSpec from pinecone_text.sparse import BM25Encoder from transformers import CLIPProcessor, CLIPModel import openai # ------------------- Keys & Setup ------------------- openai.api_key = os.getenv("OPENAI_API_KEY") pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY")) spec = ServerlessSpec(cloud=os.getenv("PINECONE_CLOUD") or "aws", region=os.getenv("PINECONE_REGION") or "us-east-1") index_name = "hybrid-image-search" if index_name not in pc.list_indexes().names(): pc.create_index(index_name, dimension=512, metric='dotproduct', spec=spec) while not pc.describe_index(index_name).status['ready']: time.sleep(1) index = pc.Index(index_name) # ------------------- Models & Dataset ------------------- fashion = load_dataset("ashraq/fashion-product-images-small", split="train") images = fashion["image"] metadata = fashion.remove_columns("image").to_pandas() bm25 = BM25Encoder() bm25.fit(metadata["productDisplayName"]) device = 'cuda' if torch.cuda.is_available() else 'cpu' model = SentenceTransformer('sentence-transformers/clip-ViT-B-32', device=device) clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device) clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") # ------------------- Helper Functions ------------------- def hybrid_scale(dense, sparse, alpha: float): if alpha < 0 or alpha > 1: raise ValueError("Alpha must be between 0 and 1") hsparse = { 'indices': sparse['indices'], 'values': [v * (1 - alpha) for v in sparse['values']] } hdense = [v * alpha for v in dense] return hdense, hsparse def extract_intent_from_openai(query: str): prompt = f""" You are an assistant for a fashion search engine. Extract the user's intent from the following query. Return a Python dictionary with keys: category, gender, subcategory, color. If something is missing, use null. Query: "{query}" Only return the dictionary. """ try: response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0 ) raw = response.choices[0].message['content'] structured = eval(raw) return structured except Exception as e: print(f"⚠️ OpenAI intent extraction failed: {e}") return {"include": {}, "exclude": {}} #-----------------below changed------------------------------# import imagehash from PIL import Image def is_duplicate(img, existing_hashes, hash_size=16, tolerance=0): """ Checks if the image is a near-duplicate based on perceptual hash. :param img: PIL Image :param existing_hashes: set of previously seen hashes :param hash_size: size of the hash (default=16 for more precision) :param tolerance: allowable Hamming distance for near-duplicates :return: (bool) whether image is duplicate """ img_hash = imagehash.phash(img, hash_size=hash_size) for h in existing_hashes: if abs(img_hash - h) <= tolerance: return True existing_hashes.add(img_hash) return False def extract_metadata_filters(query: str): query_lower = query.lower() gender = None category = None subcategory = None color = None # --- Gender Mapping --- gender_map = { "men": "Men", "man": "Men", "mens": "Men", "mans": "Men", "male": "Men", "women": "Women", "woman": "Women", "womens": "Women", "female": "Women", "boys": "Boys", "boy": "Boys", "girls": "Girls", "girl": "Girls", "kids": "Kids", "kid": "Kids", "unisex": "Unisex" } for term, mapped_value in gender_map.items(): if term in query_lower: gender = mapped_value break # --- Category Mapping --- category_map = { "shirt": "Shirts", "tshirt": "Tshirts", "t-shirt": "Tshirts", "jeans": "Jeans", "watch": "Watches", "kurta": "Kurtas", "dress": "Dresses", "trousers": "Trousers", "pants": "Trousers", "shorts": "Shorts", "footwear": "Footwear", "shoes": "Shoes", "fashion": "Apparel" } for term, mapped_value in category_map.items(): if term in query_lower: category = mapped_value break # --- SubCategory Mapping --- subCategory_list = [ "Accessories", "Apparel Set", "Bags", "Bath and Body", "Beauty Accessories", "Belts", "Bottomwear", "Cufflinks", "Dress", "Eyes", "Eyewear", "Flip Flops", "Fragrance", "Free Gifts", "Gloves", "Hair", "Headwear", "Home Furnishing", "Innerwear", "Jewellery", "Lips", "Loungewear and Nightwear", "Makeup", "Mufflers", "Nails", "Perfumes", "Sandal", "Saree", "Scarves", "Shoe Accessories", "Shoes", "Skin", "Skin Care", "Socks", "Sports Accessories", "Sports Equipment", "Stoles", "Ties", "Topwear", "Umbrellas", "Vouchers", "Wallets", "Watches", "Water Bottle", "Wristbands" ] if "topwear" in query_lower or "top" in query_lower: subcategory = "Topwear" else: query_words = query_lower.split() for subcat in subCategory_list: if subcat.lower() in query_words: subcategory = subcat break # --- Color Extraction --- color_list = [ "red", "blue", "green", "yellow", "black", "white", "orange", "pink", "purple", "brown", "grey", "beige" ] for c in color_list: if c in query_lower: color = c.capitalize() break # --- Invalid pairs --- invalid_pairs = { ("Men", "Dresses"), ("Men", "Sarees"), ("Men", "Skirts"), ("Boys", "Dresses"), ("Boys", "Sarees"), ("Girls", "Boxers"), ("Men", "Heels") } if (gender, category) in invalid_pairs: print(f"⚠️ Invalid pair: {gender} + {category}, dropping gender") gender = None # --- Fallback for missing category --- if gender and not category: category = "Apparel" # --- Refine subcategory for party/wedding-related queries --- if "party" in query_lower or "wedding" in query_lower or "cocktail" in query_lower: if subcategory in ["Loungewear and Nightwear", "Nightdress", "Innerwear"]: subcategory = None # reset it to avoid filtering into wrong items return gender, category, subcategory, color # ------------------- Search Functions ------------------- def search_fashion(query: str, alpha: float, start: int = 0, end: int = 12, gender_override: str = None): intent = extract_intent_from_openai(query) include = intent.get("include", {}) exclude = intent.get("exclude", {}) gender = include.get("gender") category = include.get("category") subcategory = include.get("subcategory") color = include.get("color") # Apply override from dropdown if gender_override: gender = gender_override # Build Pinecone filter filter = {} # Inclusion filters if gender: filter["gender"] = gender if category: if category in ["Footwear", "Shoes"]: filter["articleType"] = {"$regex": ".*(Shoe|Footwear).*"} else: filter["articleType"] = category if subcategory: filter["subCategory"] = subcategory # Step 4: Exclude irrelevant items for party-like queries query_lower = query.lower() if any(word in query_lower for word in ["party", "wedding", "cocktail", "traditional", "reception"]): filter.setdefault("subCategory", {}) if isinstance(filter["subCategory"], dict): filter["subCategory"]["$nin"] = [ "Loungewear and Nightwear", "Nightdress", "Innerwear", "Sleepwear", "Vests", "Boxers" ] if color: filter["baseColour"] = color # Exclusion filters exclude_filter = {} if exclude.get("color"): exclude_filter["baseColour"] = {"$ne": exclude["color"]} if exclude.get("subcategory"): exclude_filter["subCategory"] = {"$ne": exclude["subcategory"]} if exclude.get("category"): exclude_filter["articleType"] = {"$ne": exclude["category"]} # Combine all filters if filter and exclude_filter: final_filter = {"$and": [filter, exclude_filter]} elif filter: final_filter = filter elif exclude_filter: final_filter = exclude_filter else: final_filter = None print(f"🔍 Using filter: {final_filter} (showing {start} to {end})") # Hybrid encoding sparse = bm25.encode_queries(query) dense = model.encode(query).tolist() hdense, hsparse = hybrid_scale(dense, sparse, alpha=alpha) result = index.query( top_k=100, vector=hdense, sparse_vector=hsparse, include_metadata=True, filter=final_filter ) # Retry fallback if len(result["matches"]) == 0: print("⚠️ No results, retrying with alpha=0 sparse only") hdense, hsparse = hybrid_scale(dense, sparse, alpha=0) result = index.query( top_k=100, vector=hdense, sparse_vector=hsparse, include_metadata=True, filter=final_filter ) # Format results imgs_with_captions = [] seen_hashes = set() for r in result["matches"]: idx = int(r["id"]) img = images[idx] meta = r.get("metadata", {}) if not isinstance(img, Image.Image): img = Image.fromarray(np.array(img)) padded = ImageOps.pad(img, (256, 256), color="white") caption = str(meta.get("productDisplayName", "Unknown Product")) if not is_duplicate(padded, seen_hashes): imgs_with_captions.append((padded, caption)) if len(imgs_with_captions) >= end: break return imgs_with_captions def search_by_image(uploaded_image, alpha=0.5, start=0, end=12): # Step 1: Preprocess image for CLIP model processed = clip_processor(images=uploaded_image, return_tensors="pt").to(device) with torch.no_grad(): image_vec = clip_model.get_image_features(**processed) image_vec = image_vec.cpu().numpy().flatten().tolist() # Step 2: Query Pinecone index for similar images result = index.query( top_k=100, # fetch more to allow deduplication vector=image_vec, include_metadata=True ) matches = result["matches"] imgs_with_captions = [] seen_hashes = set() # Step 3: Deduplicate based on image hash for r in matches: idx = int(r["id"]) img = images[idx] meta = r.get("metadata", {}) caption = str(meta.get("productDisplayName", "Unknown Product")) if not isinstance(img, Image.Image): img = Image.fromarray(np.array(img)) padded = ImageOps.pad(img, (256, 256), color="white") if not is_duplicate(padded, seen_hashes): imgs_with_captions.append((padded, caption)) if len(imgs_with_captions) >= end: break return imgs_with_captions # import gradio as gr # import whisper # asr_model = whisper.load_model("base") # def handle_voice_search(vf_path, a, offset, gender_ui): # try: # transcription = asr_model.transcribe(vf_path)["text"].strip() # except: # transcription = "" # filters = extract_intent_from_openai(transcription) if transcription else {} # gender_override = gender_ui if gender_ui else filters.get("gender") # results = search_fashion(transcription, a, 0, 12, gender_override) # seen_ids = {r[1] for r in results} # return results, 12, transcription, None, gender_override, results, seen_ids # custom_css = """ # /* === Global Styling === */ # /* === Override Gradio default background === */ # /* Add soft card-like containers */ # .gr-box, .gr-block, .gr-column, .gr-row { # background-color: #ffffff !important; # border-radius: 12px; # padding: 16px !important; # box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); # } # #app-bg { # min-height: 100vh; # padding: 0; # margin: 0; # # background: radial-gradient(circle at center, #0b1f36 0%, #033e3e 100%); # display: flex; # justify-content: center; # align-items: flex-start; # background-attachment: fixed; # position: relative; # overflow: hidden; # } # #app-bg::before { # content: ""; # position: absolute; # top: 0; left: 0; # width: 100%; height: 100%; # background: radial-gradient(circle at center, rgba(0, 255, 255, 0.08), transparent); # z-index: 0; # } # #main-container { # z-index: 1; # position: relative; # } # /* === Heading Style === */ # h1, .gr-markdown h1 { # font-size: 2.2rem !important; # font-weight: bold; # color: #000000; # text-align: center; # margin-bottom: 1rem; # } # /* === Tabs === */ # .gr-tab { # border-radius: 12px !important; # background-color: #ffffff !important; # box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); # padding: 16px !important; # margin-top: 12px; # } # /* === Textbox, Dropdown, Slider === */ # input[type="text"], .gr-textbox textarea, .gr-dropdown, .gr-slider { # border-radius: 8px !important; # border: 1px solid #ccc !important; # padding: 10px !important; # font-size: 16px; # box-shadow: 0 1px 3px rgba(0,0,0,0.05); # } # /* === Image Upload === */ # .gr-image { # width: 100% !important; # max-width: 100% !important; # border-radius: 12px; # box-shadow: 0 2px 10px rgba(0,0,0,0.1); # } # /* === Buttons (custom style .button-36) === */ # .gr-button { # background-color: #DBDBDB !important; # background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%); # border-radius: 8px !important; # border-style: none !important; # box-sizing: border-box; # color: #FFFFFF !important; # cursor: pointer; # flex-shrink: 0; # font-family: "Inter UI","SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif; # font-size: 16px; # font-weight: 500; # height: 4rem; # padding: 0 1.6rem; # text-align: center; # text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px; # transition: all .5s; # user-select: none; # -webkit-user-select: none; # touch-action: manipulation; # } # .gr-button:hover { # box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px; # transition-duration: .1s; # } # /* === Responsive padding === */ # @media (min-width: 768px) { # .gr-button { # padding: 0 2.6rem; # } # } # /* === Gallery Grid === */ # .gr-gallery { # padding-top: 12px; # } # .gr-gallery-item { # width: 128px !important; # height: 128px !important; # transition: transform 0.3s ease-in-out; # border-radius: 8px; # overflow: hidden; # } # .gr-gallery-item:hover { # transform: scale(1.06); # box-shadow: 0 3px 12px rgba(0,0,0,0.15); # } # .gr-gallery-item img { # object-fit: cover !important; # width: 100% !important; # height: 100% !important; # border-radius: 8px; # } # /* === Audio Upload === */ # .gr-audio { # width: 100% !important; # border-radius: 12px; # background-color: #fff !important; # box-shadow: 0 1px 5px rgba(0,0,0,0.1); # } # /* === Footer === */ # .gr-markdown:last-child { # text-align: center; # font-size: 14px; # color: #666; # padding-top: 1rem; # } # #main-container { # width: 95%; # max-width: 1100px; # margin: 20px auto !important; # padding: 16px; # background: #ffffff; # border-radius: 18px; # box-shadow: 0 10px 30px rgba(0,0,0,0.08); # border: 3px solid orange; # # overflow-y: auto; # # max-height: 90vh; # } # /* For phones and smaller devices */ # @media (max-width: 768px) { # #main-container { # width: 100%; # margin: 8px; # padding: 12px; # border-radius: 12px; # max-height: none; # } # .gr-button { # font-size: 14px; # height: 3.2rem; # } # input[type="text"], .gr-textbox textarea, .gr-dropdown, .gr-slider { # font-size: 14px; # padding: 8px !important; # } # h1, .gr-markdown h1 { # font-size: 1.6rem !important; # } # .gr-gallery-item { # width: 100px !important; # height: 100px !important; # } # .gr-image { # height: auto !important; # } # } # /* === Tab Label Styling === */ # button[role="tab"] { # color: #000000 !important; /* Default tab text color: black */ # font-weight: 500; # transition: color 0.3s ease-in-out; # font-size: 16px; # } # /* Active tab title */ # button[role="tab"][aria-selected="true"] { # color: #f57c00 !important; /* Active tab text color: orange */ # font-weight: bold !important; # } # /* Hover effect on tab titles */ # button[role="tab"]:hover { # color: #f57c00 !important; /* Orange on hover */ # font-weight: 600; # cursor: pointer; # } # /* === Uniform Input Sizes for Text, Audio, Image === */ # .gr-textbox, .gr-audio, .gr-image { # max-width: 100% !important; # width: 100% !important; # } # .gr-audio, .gr-image { # max-width: 500px !important; # margin: 0 auto; # } # .gr-image { # height: 256px !important; # } # """ # with gr.Blocks(css=custom_css) as demo: # with gr.Column(elem_id="app-bg"): # with gr.Column(elem_id="main-container"): # gr.Markdown("# 🛍️ Fashion Product Hybrid Search") # alpha = gr.Slider(0, 1, value=0.5, label="Hybrid Weight (alpha: 0=sparse, 1=dense)") # with gr.Tabs(): # with gr.Tab("Text Search"): # query = gr.Textbox( # label="Text Query", # placeholder="e.g., floral summer dress for women" # ) # gender_dropdown = gr.Dropdown( # ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], # label="Gender Filter (optional)" # ) # text_search_btn = gr.Button("Search by Text", elem_classes="search-btn") # with gr.Tab("🎙️ Voice Search"): # voice_input = gr.Audio(label="Speak Your Query", type="filepath") # voice_gender_dropdown = gr.Dropdown(["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender") # voice_search_btn = gr.Button("Search by Voice") # with gr.Tab("Image Search"): # # image_input = gr.Image( # # type="pil", # # label="Upload an image", # # sources=["upload", "clipboard"], # # height=256, # # width=356 # # ) # image_input = gr.Image( # type="pil", # label="Upload an image", # sources=["upload", "clipboard"], # # tool=None, # height=400 # ) # image_gender_dropdown = gr.Dropdown( # ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], # label="Gender Filter (optional)" # ) # image_search_btn = gr.Button("Search by Image", elem_classes="search-btn") # gallery = gr.Gallery(label="Search Results", columns=6, height=None) # load_more_btn = gr.Button("Load More") # # --- UI State Holders --- # search_offset = gr.State(0) # current_query = gr.State("") # current_image = gr.State(None) # current_gender = gr.State("") # shown_results = gr.State([]) # shown_ids = gr.State(set()) # # --- Unified Search Function --- # def unified_search(q, uploaded_image, a, offset, gender_ui): # start = 0 # end = 12 # filters = extract_intent_from_openai(q) if q.strip() else {} # gender_override = gender_ui if gender_ui else filters.get("gender") # if uploaded_image is not None: # results = search_by_image(uploaded_image, a, start, end) # elif q.strip(): # results = search_fashion(q, a, start, end, gender_override) # else: # results = [] # seen_ids = {r[1] for r in results} # return results, end, q, uploaded_image, gender_override, results, seen_ids # # Text Search # # Text Search # text_search_btn.click( # unified_search, # inputs=[query, gr.State(None), alpha, search_offset, gender_dropdown], # outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] # ) # voice_search_btn.click( # handle_voice_search, # inputs=[voice_input, alpha, search_offset, voice_gender_dropdown], # outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] # ) # # Image Search # image_search_btn.click( # unified_search, # inputs=[gr.State(""), image_input, alpha, search_offset, image_gender_dropdown], # outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] # ) # # --- Load More Button --- # def load_more_fn(a, offset, q, img, gender_ui, prev_results, prev_ids): # start = offset # end = offset + 12 # gender_override = gender_ui # if img is not None: # new_results = search_by_image(img, a, start, end) # elif q.strip(): # new_results = search_fashion(q, a, start, end, gender_override) # else: # new_results = [] # filtered_new = [] # new_ids = set() # for item in new_results: # img_obj, caption = item # if caption not in prev_ids: # filtered_new.append(item) # new_ids.add(caption) # combined = prev_results + filtered_new # updated_ids = prev_ids.union(new_ids) # return combined, end, combined, updated_ids # load_more_btn.click( # load_more_fn, # inputs=[alpha, search_offset, current_query, current_image, current_gender, shown_results, shown_ids], # outputs=[gallery, search_offset, shown_results, shown_ids] # ) # # gr.Markdown("🧠 Powered by OpenAI + Hybrid AI Fashion Search") # demo.launch() import gradio as gr import whisper asr_model = whisper.load_model("base") def handle_voice_search(vf_path, a, offset, gender_ui): try: transcription = asr_model.transcribe(vf_path)["text"].strip() except: transcription = "" filters = extract_intent_from_openai(transcription) if transcription else {} gender_override = gender_ui if gender_ui else filters.get("gender") results = search_fashion(transcription, a, 0, 12, gender_override) seen_ids = {r[1] for r in results} return results, 12, transcription, None, gender_override, results, seen_ids custom_css = """ /* === Background Styling === */ # html, body { # margin: 0; # padding: 0; # height: 100%; # overflow: auto; # } html, body { height: auto; min-height: 100%; overflow-x: hidden; } # #app-bg { # min-height: 100vh; # display: flex; # justify-content: center; # align-items: flex-start; # background: radial-gradient(circle at center, #C2C5EF 0%, #E0E2F5 100%) !important; # background-attachment: fixed; # position: relative; # overflow-y: auto; # padding: 24px; # } #app-bg { background: radial-gradient(circle at center, #C2C5EF 0%, #E0E2F5 100%) !important; background-attachment: fixed; padding: 24px; width: 100%; } /* === Main Content Container === */ # #main-container { # width: 95%; # max-width: 1100px; # margin: 20px auto; # padding: 24px; # background: #ffffff; # border-radius: 18px; # box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); # # border: 2px solid #C2C5EF; # border: 2px solid black; # position: relative; # z-index: 1; # overflow: visible; # } #main-container { width: 95%; max-width: 1100px; margin: 20px auto; padding: 24px; background: #ffffff; border-radius: 18px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); # border: 2px solid #C2C5EF; border: 2px solid black; } /* === Card Containers === */ .gr-box, .gr-block, .gr-column, .gr-row, .gr-tab { background-color: #C2C5EF !important; color: #22284F !important; border-radius: 12px; padding: 16px !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } /* === Headings === */ h1, .gr-markdown h1 { font-size: 2.2rem !important; font-weight: bold; color: #22284F; text-align: center; margin-bottom: 1rem; } /* === Inputs === */ input[type="text"], .gr-textbox textarea, .gr-dropdown, .gr-slider { background-color: #C2C5EF !important; color: #22284F !important; border-radius: 8px; border: 1px solid #999 !important; padding: 10px !important; font-size: 16px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); } /* === Gallery Grid === */ .gr-gallery { padding-top: 12px; overflow-y: auto; } .gr-gallery-item { width: 128px !important; height: 128px !important; border-radius: 8px; overflow: hidden; background-color: #C2C5EF; color: #22284F; transition: transform 0.3s ease-in-out; } .gr-gallery-item:hover { transform: scale(1.06); box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15); } .gr-gallery-item img { object-fit: cover; width: 100%; height: 100%; border-radius: 8px; } /* === Audio & Image === */ .gr-audio, .gr-image { width: 100% !important; max-width: 500px !important; margin: 0 auto; border-radius: 12px; background-color: #C2C5EF !important; color: #22284F !important; box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1); } .gr-image { height: 256px !important; } /* === Buttons === */ .gr-button { background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%); color: #ffffff !important; border-radius: 8px; font-size: 16px; font-weight: 500; height: 3.5rem; padding: 0 1.5rem; border: none; box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px; transition: all 0.3s; } .gr-button:hover { transform: translateY(-2px); box-shadow: rgba(80, 63, 205, 0.8) 0 2px 20px; } /* === Tab Labels === */ button[role="tab"] { color: #22284F !important; font-weight: 500; font-size: 16px; } button[role="tab"][aria-selected="true"] { color: #f57c00 !important; font-weight: bold; } button[role="tab"]:hover { color: #f57c00 !important; font-weight: 600; cursor: pointer; } /* === Footer === */ .gr-markdown:last-child { text-align: center; font-size: 14px; color: #666; padding-top: 1rem; } /* === Responsive === */ @media (max-width: 768px) { #main-container { width: 100%; margin: 8px; padding: 12px; } .gr-button { font-size: 14px; height: 3.2rem; } input[type="text"], .gr-textbox textarea, .gr-dropdown, .gr-slider { font-size: 14px; padding: 8px !important; } h1, .gr-markdown h1 { font-size: 1.6rem !important; } .gr-gallery-item { width: 100px !important; height: 100px !important; } .gr-image { height: auto !important; } } """ with gr.Blocks(css=custom_css) as demo: with gr.Column(elem_id="app-bg"): with gr.Column(elem_id="main-container"): gr.Markdown("# 🛍️ Fashion Product Hybrid Search") alpha = gr.Slider(0, 1, value=0.5, label="Hybrid Weight (alpha: 0=sparse, 1=dense)") with gr.Tabs(): with gr.Tab("Text Search"): query = gr.Textbox( label="Text Query", placeholder="e.g., floral summer dress for women" ) gender_dropdown = gr.Dropdown( ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender Filter (optional)" ) text_search_btn = gr.Button("Search by Text", elem_classes="search-btn") with gr.Tab("🎙️ Voice Search"): voice_input = gr.Audio(label="Speak Your Query", type="filepath") voice_gender_dropdown = gr.Dropdown(["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender") voice_search_btn = gr.Button("Search by Voice") with gr.Tab("Image Search"): # image_input = gr.Image( # type="pil", # label="Upload an image", # sources=["upload", "clipboard"], # height=256, # width=356 # ) image_input = gr.Image( type="pil", label="Upload an image", sources=["upload", "clipboard"], # tool=None, height=400 ) image_gender_dropdown = gr.Dropdown( ["", "Men", "Women", "Boys", "Girls", "Kids", "Unisex"], label="Gender Filter (optional)" ) image_search_btn = gr.Button("Search by Image", elem_classes="search-btn") gallery = gr.Gallery(label="Search Results", columns=6, height=None) load_more_btn = gr.Button("Load More") # --- UI State Holders --- search_offset = gr.State(0) current_query = gr.State("") current_image = gr.State(None) current_gender = gr.State("") shown_results = gr.State([]) shown_ids = gr.State(set()) # --- Unified Search Function --- def unified_search(q, uploaded_image, a, offset, gender_ui): start = 0 end = 12 filters = extract_intent_from_openai(q) if q.strip() else {} gender_override = gender_ui if gender_ui else filters.get("gender") if uploaded_image is not None: results = search_by_image(uploaded_image, a, start, end) elif q.strip(): results = search_fashion(q, a, start, end, gender_override) else: results = [] seen_ids = {r[1] for r in results} return results, end, q, uploaded_image, gender_override, results, seen_ids # Text Search # Text Search text_search_btn.click( unified_search, inputs=[query, gr.State(None), alpha, search_offset, gender_dropdown], outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] ) voice_search_btn.click( handle_voice_search, inputs=[voice_input, alpha, search_offset, voice_gender_dropdown], outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] ) # Image Search image_search_btn.click( unified_search, inputs=[gr.State(""), image_input, alpha, search_offset, image_gender_dropdown], outputs=[gallery, search_offset, current_query, current_image, current_gender, shown_results, shown_ids] ) # --- Load More Button --- def load_more_fn(a, offset, q, img, gender_ui, prev_results, prev_ids): start = offset end = offset + 12 gender_override = gender_ui if img is not None: new_results = search_by_image(img, a, start, end) elif q.strip(): new_results = search_fashion(q, a, start, end, gender_override) else: new_results = [] filtered_new = [] new_ids = set() for item in new_results: img_obj, caption = item if caption not in prev_ids: filtered_new.append(item) new_ids.add(caption) combined = prev_results + filtered_new updated_ids = prev_ids.union(new_ids) return combined, end, combined, updated_ids load_more_btn.click( load_more_fn, inputs=[alpha, search_offset, current_query, current_image, current_gender, shown_results, shown_ids], outputs=[gallery, search_offset, shown_results, shown_ids] ) # gr.Markdown("🧠 Powered by OpenAI + Hybrid AI Fashion Search") demo.launch()