import gradio as gr import pandas as pd import numpy as np from openai import OpenAI import ast, os # ── Load dataset ────────────────────────────────────────────────────────────── df = pd.read_parquet("steam_games_clustered_production.parquet") # Normalise embeddings to a (N, 768) float32 matrix for fast cosine search embeddings_matrix = np.stack(df["embedding"].values).astype(np.float32) norms = np.linalg.norm(embeddings_matrix, axis=1, keepdims=True) norms[norms == 0] = 1e-9 embeddings_matrix_normed = embeddings_matrix / norms # Cluster descriptions (from README) CLUSTER_LABELS = { 0: "🏟️ Sports & Simulation", 1: "🎮 Mainstream / Cinematic", 2: "🀄 East Asian Art Style", 3: "🌸 Anime & 2D Illustration", -1: "🌀 Unique / Outliers", } client = OpenAI() # reads OPENAI_API_KEY from env automatically # ── Core helpers ────────────────────────────────────────────────────────────── def embed_text(text: str) -> np.ndarray: """Embed a user text query with OpenAI text-embedding-3-small.""" response = client.embeddings.create( model="text-embedding-3-small", input=text, dimensions=768, # match our 768-dim CLIP vectors ) vec = np.array(response.data[0].embedding, dtype=np.float32) return vec / (np.linalg.norm(vec) + 1e-9) def cosine_search(query_vec: np.ndarray, top_k: int = 6) -> pd.DataFrame: """Return the top-k most similar games via cosine similarity.""" sims = embeddings_matrix_normed @ query_vec # (N,) idx = np.argsort(sims)[::-1][:top_k] result = df.iloc[idx].copy() result["similarity"] = sims[idx] return result def build_game_card(row) -> str: """Build one HTML game card.""" genres = ", ".join(row["genres"]) if hasattr(row["genres"], "__iter__") and not isinstance(row["genres"], str) else row["genres"] cluster = CLUSTER_LABELS.get(int(row["cluster_label"]), "❓ Unknown") sim_pct = f"{row['similarity']*100:.1f}%" recs = f"{int(row['recommendations']):,}" img_url = row["header_image"] steam_name = row["name"].replace(" ", "_") # rough slug (Steam search) search_url = f"https://store.steampowered.com/search/?term={row['name'].replace(' ', '+')}" return f"""
""" CARD_CSS = """ """ def search_games(query: str, top_k: int, cluster_filter: str): """Main search function wired to Gradio.""" query = query.strip() if not query: return CARD_CSS + '✨ Describe an art style or game vibe above to explore the universe.
' try: query_vec = embed_text(query) except Exception as e: return CARD_CSS + f'⚠️ OpenAI embedding error: {e}
' # Expand top_k when filtering so we still show `top_k` results after filter fetch_k = top_k * 6 if cluster_filter != "All Clusters" else top_k results = cosine_search(query_vec, top_k=min(fetch_k, len(df))) # Optional cluster filter if cluster_filter != "All Clusters": cluster_id = {v: k for k, v in CLUSTER_LABELS.items()}.get(cluster_filter, None) if cluster_id is not None: results = results[results["cluster_label"] == cluster_id] results = results.head(top_k) if results.empty: return CARD_CSS + '🔭 No games found for that filter. Try a broader cluster or different query.
' cards_html = "".join(build_game_card(row) for _, row in results.iterrows()) header = f'🎯 Top {len(results)} visual matches for "{query}"
' return CARD_CSS + header + f'Discover games by art style, mood & visual aesthetic — powered by CLIP embeddings
') with gr.Row(): with gr.Column(scale=5): query_input = gr.Textbox( placeholder='e.g. "dark gothic pixel art", "vibrant anime pastel", "gritty photorealistic military"…', label="Describe the game aesthetic", lines=2, max_lines=4, ) with gr.Column(scale=2, min_width=180): cluster_dropdown = gr.Dropdown( choices=CLUSTER_CHOICES, value="All Clusters", label="Filter by Aesthetic Cluster", ) with gr.Column(scale=1, min_width=120): top_k_slider = gr.Slider( minimum=3, maximum=12, step=1, value=6, label="Results", ) search_btn = gr.Button("🔭 Explore", variant="primary", elem_id="search-btn") output_html = gr.HTML( value=CARD_CSS + '✨ Describe an art style or game vibe above to explore the universe.
' ) # Example prompts gr.Examples( examples=[ ["dark gothic pixel art with neon accents"], ["vibrant anime pastel soft character art"], ["gritty photorealistic military shooter"], ["cozy watercolor hand-drawn indie"], ["east asian ink brush calligraphy"], ["retro 8-bit arcade sprites"], ["sci-fi space neon cyberpunk"], ["sports simulation athlete photography"], ], inputs=query_input, label="💡 Example Aesthetic Queries", ) # Wire events search_btn.click( fn=search_games, inputs=[query_input, top_k_slider, cluster_dropdown], outputs=output_html, ) query_input.submit( fn=search_games, inputs=[query_input, top_k_slider, cluster_dropdown], outputs=output_html, ) if __name__ == "__main__": demo.launch()