HM-Style-Scout / app.py
lia-prop13's picture
Update app.py
0ea19b5 verified
import pandas as pd
import torch
import torch.nn.functional as F
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import gradio as gr
from datasets import load_dataset
# ============================================================
# 1. INITIALIZATION (Synced with Research Environment)
# ============================================================
device = "cuda" if torch.cuda.is_available() else "cpu"
# Load the CLIP model and processor using the Transformers library
# Ensuring 100% parity with the embeddings generated in Colab
MODEL_ID = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(MODEL_ID)
model = CLIPModel.from_pretrained(MODEL_ID).to(device)
# Load metadata and the reference dataset
df = pd.read_parquet('hm_style_data.parquet')
ds = load_dataset("tomytjandra/h-and-m-fashion-caption", split="train")
# ============================================================
# 2. ENGINE (Standardized Vector Generation with Object Fix)
# ============================================================
def get_embedding(user_input, input_type):
"""
Generates a normalized feature vector.
Includes a safety check to extract the tensor from model output objects.
"""
model.eval()
with torch.no_grad():
if input_type == "image":
# Process image and extract features
image = Image.open(user_input).convert("RGB")
inputs = processor(images=image, return_tensors="pt").to(device)
outputs = model.get_image_features(**inputs)
else:
# Process text and extract features
inputs = processor(text=[user_input], return_tensors="pt", padding=True).to(device)
outputs = model.get_text_features(**inputs)
# --- THE COLAB FIX: Ensure we are working with a Tensor, not an Object ---
if not isinstance(outputs, torch.Tensor):
embedding = getattr(outputs, "pooler_output", outputs)
else:
embedding = outputs
# Apply L2 Normalization using the functional interface
embedding = F.normalize(embedding, p=2, dim=-1)
return embedding.cpu().numpy().flatten()
def search_styles(text_input, image_input):
try:
if image_input:
user_vector = get_embedding(image_input, "image")
elif text_input:
user_vector = get_embedding(text_input, "text")
else:
return None, "Upload inspiration to begin..."
stored_embeddings = np.stack(df['embedding'].values)
scores = cosine_similarity(user_vector.reshape(1, -1), stored_embeddings).flatten()
df['similarity_score'] = scores
top_matches = df.sort_values(by='similarity_score', ascending=False).head(4)
return [ds[int(row['item_id'])]['image'] for _, row in top_matches.iterrows()], ""
except Exception as e:
return None, f"Status: {str(e)}"
# ============================================================
# 3. UI DESIGN
# ============================================================
BG_URL = "https://huggingface.co/spaces/lia-prop13/HM-Style-Scout/resolve/main/background.png"
custom_css = f"""
/* 1. Global Background & Footer Removal */
.gradio-container, body, #component-0 {{
background-image: url('{BG_URL}') !important;
background-repeat: repeat !important;
background-size: 550px !important;
background-attachment: fixed !important;
background-color: #f3f3f3 !important;
}}
footer {{ display: none !important; }}
/* 2. Glass Card UI */
.main-card {{
background: rgba(255, 255, 255, 0.96) !important;
backdrop-filter: blur(20px);
border-radius: 40px !important;
padding: 30px 50px !important;
margin: 15px auto !important;
box-shadow: 0 30px 60px rgba(0,0,0,0.1) !important;
max-width: 950px !important;
}}
/* 3. Typography */
.hm-title {{ font-family: 'Inter', sans-serif; font-weight: 900; font-size: 36px; text-align: center; color: #000; margin-bottom: 5px !important; }}
.hm-subtitle {{ font-family: 'Inter', sans-serif; font-weight: 400; font-size: 15px; text-align: center; color: #444; margin-bottom: 5px; }}
.hm-pro-tip {{ font-family: 'Inter', sans-serif; font-size: 13px; text-align: center; color: #777; font-style: italic; margin-bottom: 25px; }}
/* 4. Image Upload - Neutralizing Buttons & Hiding Toolbar */
.image-upload {{ border: 1px solid rgba(0,0,0,0.05) !important; border-radius: 25px !important; background: white !important; overflow: hidden; }}
/* Hiding the bar and labels */
.image-upload label, .image-upload .image-footer, .image-upload .icon-buttons, .image-upload .icon-button {{
display: none !important;
}}
/* If buttons still appear, make them neutral (Not Orange) */
.image-upload button, .image-upload .image-footer button {{
color: #000 !important;
background: transparent !important;
border: none !important;
}}
.image-upload .upload-text {{ visibility: hidden !important; }}
.image-upload .upload-text::after {{
content: "Upload or drag image";
visibility: visible !important;
display: block !important;
font-size: 15px !important;
color: #888 !important;
}}
/* 5. Gallery & Interaction (No Orange Focus) */
.gallery-container, .gallery-container * {{ border: none !important; outline: none !important; box-shadow: none !important; background: transparent !important; }}
.gallery-container img {{ transition: transform 0.4s ease !important; border-radius: 12px !important; }}
.gallery-container img:hover {{ transform: scale(1.05) !important; }}
/* 6. Buttons & Inputs */
.search-button {{ background: #000 !important; color: #fff !important; border-radius: 30px !important; height: 50px !important; font-weight: 700 !important; margin-top: 15px !important; border: none !important; }}
.text-input {{ border: 1px solid rgba(0,0,0,0.05) !important; border-radius: 15px !important; margin-top: 10px !important; }}
/* Heights Symmetry */
.image-upload {{ height: 300px !important; }}
.gallery-container {{ height: 435px !important; }}
"""
with gr.Blocks(css=custom_css) as demo:
with gr.Column(elem_classes="main-card"):
gr.HTML("""
<div class='hm-title'>H&M Style Matcher</div>
<div class='hm-subtitle'>Upload a photo of a clothing item or describe your dream fit. We'll find your next H&M favorite in seconds.</div>
<div class='hm-pro-tip'> Pro tip: Search using either text OR an image. Remember to clear one before trying the other!</div>
""")
with gr.Row(equal_height=True):
with gr.Column(scale=2):
input_img = gr.Image(
label=None,
type="filepath",
show_label=False,
container=False,
elem_classes="image-upload"
)
input_txt = gr.Textbox(
placeholder="Type your style inspiration here...",
show_label=False,
container=False,
elem_classes="text-input"
)
search_btn = gr.Button("FIND MATCHES", elem_classes="search-button")
status = gr.Markdown("")
with gr.Column(scale=3):
style_gallery = gr.Gallery(
show_label=False,
columns=2,
object_fit="contain",
elem_classes="gallery-container"
)
search_btn.click(
fn=search_styles,
inputs=[input_txt, input_img],
outputs=[style_gallery, status]
)
if __name__ == "__main__":
demo.launch()