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"""
{row['name']}
{sim_pct} match

{row['name']}

{genres}

{cluster} 👍 {recs}
""" 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'
{cards_html}
' # ── Gradio UI ───────────────────────────────────────────────────────────────── THEME = gr.themes.Base( primary_hue=gr.themes.colors.cyan, secondary_hue=gr.themes.colors.indigo, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Rajdhani"), gr.themes.GoogleFont("Inter"), "sans-serif"], ).set( body_background_fill="#0d1117", body_text_color="#e8eaf6", block_background_fill="#161b27", block_border_color="#2a3454", input_background_fill="#1f2638", input_border_color="#2a3454", button_primary_background_fill="#4fc3f7", button_primary_text_color="#050810", button_primary_background_fill_hover="#7c4dff", ) CLUSTER_CHOICES = ["All Clusters"] + list(CLUSTER_LABELS.values()) with gr.Blocks( theme=THEME, title="🌌 Steam Graphic Universe Explorer", css=""" #title { text-align:center; font-family:'Rajdhani',sans-serif; font-size:2.4rem; font-weight:700; letter-spacing:.06em; background: linear-gradient(90deg,#4fc3f7,#7c4dff); -webkit-background-clip:text; -webkit-text-fill-color:transparent; margin-bottom:4px; } #subtitle { text-align:center; color:#8898b3; font-size:.95rem; margin-bottom:28px; } #search-btn { min-width:120px; font-family:'Rajdhani',sans-serif; font-weight:700; letter-spacing:.1em; text-transform:uppercase; } """ ) as demo: gr.HTML('

🌌 Steam Graphic Universe

') gr.HTML('

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()