Spaces:
Running
Running
# 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() | |