Hardik-25's picture
Upload 15 files
f30e1e4 verified
Raw
History Blame Contribute Delete
36.5 kB
import gradio as gr
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import html
import traceback
# Import custom modules
from data_loader import load_movie_data, get_trending_movies, get_kpi_statistics
from styles import CUSTOM_CSS, NAVBAR_HTML, FOOTER_HTML
from utils import filter_and_sort_movies, paginate_dataframe, fuzzy_search_movies
from components import (
build_movie_grid_html,
build_movie_details_html,
build_recommendations_grid_html,
build_similar_grid_html,
get_rating_distribution_chart,
get_year_distribution_chart,
get_predictions_score_chart,
get_movie_relationship_graph,
get_single_movie_rating_distribution,
get_single_movie_popularity_timeline,
get_diagnostics_html
)
from recommender import RecommenderManager
from poster_service import poster_service
# Initialize data and model manager
df_raw = load_movie_data("movie_metadata.parquet")
recommender = RecommenderManager()
print("Loading recommender model (this may take 30-60 seconds)...", flush=True)
recommender.load_resources()
print("Recommender ready.", flush=True)
# Pre-calculate catalog statistics
kpis = get_kpi_statistics(df_raw)
trending_df = get_trending_movies(df_raw, limit=20)
# Determine year range boundaries
valid_years = df_raw[df_raw["year"] > 0]["year"]
min_year = int(valid_years.min()) if not valid_years.empty else 1900
max_year = int(valid_years.max()) if not valid_years.empty else 2006
# Number of cards per page
PAGE_SIZE = 20
def get_page_label(current, total):
return f"Page {current + 1} of {total}"
# Callback handlers for Movie Explorer Catalog
def filter_catalog(query, year_min, year_max, min_rating, min_rating_count, sort_by):
"""
Filters the movie explorer database and returns page 0 results.
"""
year_range = [year_min, year_max]
filtered, search_info = filter_and_sort_movies(df_raw, query, year_range, min_rating, min_rating_count, sort_by)
# Build status banner based on match type
msg = ""
if search_info and search_info["match_type"] and query.strip():
mt = search_info["match_type"]
q = html.escape(query.strip())
count = len(filtered)
if mt == "EXACT_MATCH":
msg = (
f'<div class="search-status" style="background:rgba(76,175,80,0.08);border:1px solid rgba(76,175,80,0.25);'
f'border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:14px;">'
f'<span style="color:#4CAF50;font-weight:700;">Results for "{q}"</span>'
f'</div>'
)
elif mt == "FRANCHISE_MATCH":
msg = (
f'<div class="search-status" style="background:rgba(33,150,243,0.08);border:1px solid rgba(33,150,243,0.25);'
f'border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:14px;">'
f'<span style="color:#42A5F5;font-weight:700;">Found {count} related titles for "{q}"</span>'
f'</div>'
)
elif mt == "PARTIAL_MATCH":
msg = (
f'<div class="search-status" style="background:rgba(255,183,77,0.1);border:1px solid rgba(255,183,77,0.3);'
f'border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:14px;">'
f'<span style="color:#FFB74D;font-weight:700;">"{q}"</span> '
f'<span style="color:var(--text-secondary);">is not available. Showing related titles instead.</span>'
f'</div>'
)
elif mt == "NO_MATCH":
msg = (
f'<div class="search-status" style="background:rgba(255,100,100,0.08);border:1px solid rgba(255,100,100,0.25);'
f'border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:14px;text-align:center;">'
f'<span style="color:#FF6B6B;font-weight:700;">No matches found for "{q}"</span>'
f'</div>'
)
# Paginate page 0
sliced, total_pages = paginate_dataframe(filtered, 0, PAGE_SIZE)
grid_html = msg + build_movie_grid_html(sliced)
label = get_page_label(0, total_pages)
# Disable/enable pagination buttons based on total pages
prev_interactive = False
next_interactive = total_pages > 1
return filtered, 0, grid_html, label, gr.update(interactive=prev_interactive), gr.update(interactive=next_interactive)
def change_page(direction, current_page, filtered_df):
"""
Paginates the filtered dataframe forward or backward.
"""
total_records = len(filtered_df)
total_pages = max(1, int(np.ceil(total_records / PAGE_SIZE)))
new_page = current_page + direction
new_page = max(0, min(new_page, total_pages - 1))
sliced, _ = paginate_dataframe(filtered_df, new_page, PAGE_SIZE)
grid_html = build_movie_grid_html(sliced)
label = get_page_label(new_page, total_pages)
prev_interactive = new_page > 0
next_interactive = new_page < (total_pages - 1)
return new_page, grid_html, label, gr.update(interactive=prev_interactive), gr.update(interactive=next_interactive)
def select_movie(movie_id):
"""
Queries details for a selected movie ID and renders the details panel along with Plotly charts.
"""
if not movie_id or str(movie_id).strip() == "":
return build_movie_details_html(None), go.Figure(), go.Figure()
try:
m_id = int(float(movie_id))
profile = recommender.get_movie_detailed_profile(m_id)
if profile is not None:
details_html = build_movie_details_html(profile)
dist_fig = get_single_movie_rating_distribution(profile["distribution"], profile["title"])
time_fig = get_single_movie_popularity_timeline(profile["timeline"], profile["title"])
return details_html, dist_fig, time_fig
except Exception as e:
print(f"Error loading profile for movie ID {movie_id}: {e}")
return build_movie_details_html(None), go.Figure(), go.Figure()
# Section 2: DeepFM Recommendations Callback
def generate_recommendations(user_id_str, top_n_str):
"""
Runs DeepFM prediction model or retrieves cached recommendations for the User.
Also returns metrics KPIs and Plotly analytics histograms.
"""
try:
user_id = int(user_id_str.split()[0]) # handle spaces or "New User"
except ValueError:
user_id = 6 # fallback default user
try:
top_n = int(top_n_str)
except ValueError:
top_n = 10
# Generate recommendations using recommender module
try:
recs_df, is_cold = recommender.recommend_for_user(user_id, top_n=top_n)
except Exception as e:
print(f"Recommendation error: {e}", flush=True)
return (
f"<div style='text-align:center;padding:40px;color:var(--text-secondary);'>Error generating recommendations: {e}</div>",
"<div style='text-align:center;padding:20px;color:var(--text-secondary);'>Metrics unavailable</div>",
go.Figure(),
"<div style='text-align:center;padding:20px;color:var(--text-secondary);'>Error occurred</div>"
)
# Calculate metrics
metrics = recommender.get_recommendation_analytics(recs_df)
# Render outputs
grid_html = build_recommendations_grid_html(recs_df)
metrics_html = f"""
<div class="stats-grid" style="margin-top: 15px;">
<div class="stat-item">
<div class="stat-value accent">{metrics['avg_confidence']:.1f}%</div>
<div class="stat-label">Model Confidence</div>
</div>
<div class="stat-item">
<div class="stat-value">{metrics['diversity_score']:.1f}%</div>
<div class="stat-label">Diversity Index</div>
</div>
<div class="stat-item">
<div class="stat-value">{metrics['personalization_score']:.1f}%</div>
<div class="stat-label">Personalization Fit</div>
</div>
</div>
"""
# Update Plotly Chart
fig = get_predictions_score_chart(recs_df)
rec_type_badge = "Cold Start Recommendation (Popular Content)" if is_cold else "DeepFM Neural Recommendation Model"
explanation_banner = f"""
<div style="background-color: var(--surface-color); padding: 15px 20px; border-radius: 8px; border-left: 4px solid var(--accent-color); margin-bottom: 20px; font-size: 14px;">
<strong>Recommendation Mode:</strong> {rec_type_badge}<br>
<span style="color: var(--text-secondary); margin-top: 4px; display: inline-block;">
This profile is built dynamically by combining factorization low-order mappings with high-order neural net predictions.
</span>
</div>
"""
return grid_html, metrics_html, fig, explanation_banner
# Section 3: Similar Movies Autocomplete & Search Callback
def check_search_match_type(query, valid_rows):
if not valid_rows:
return "NO_MATCH", None
query_clean = query.strip().lower()
# 1. Exact Match (case-insensitive)
for title, year_str, movie_id, choice_str in valid_rows:
if title.strip().lower() == query_clean:
return "EXACT_MATCH", (title, year_str, movie_id, choice_str)
# 2. One Match
if len(valid_rows) == 1:
title, year_str, movie_id, choice_str = valid_rows[0]
title_clean = title.strip().lower()
if (query_clean in title_clean or title_clean in query_clean) and len(query_clean) >= 0.5 * len(title_clean):
return "ONE_MATCH", (title, year_str, movie_id, choice_str)
return "MULTIPLE_MATCHES", None
def handle_similar_search_change(query, similar_count_str="10"):
search_query = query
print(f"--- Similar Movies Search Triggered ---")
print(f"search_query: {repr(search_query)}")
# Initialize default outputs
empty_dropdown = gr.update(choices=[], value=None, label="Select Matching Movie")
no_results_grid = "<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Search and select a movie above, then click Find Similar.</div>"
empty_graph = go.Figure()
empty_banner = ""
if not query or len(query.strip()) < 2:
return empty_dropdown, no_results_grid, empty_graph, empty_banner
try:
top_n = int(similar_count_str)
except Exception:
top_n = 10
try:
# Call backend search
raw_res = fuzzy_search_movies(df_raw, query)
if isinstance(raw_res, tuple):
raw_matches, search_info = raw_res
else:
raw_matches = raw_res
search_info = {}
match_count = len(raw_matches)
dataframe_columns = list(raw_matches.columns)
print(f"raw_matches type: {type(raw_matches)}")
print(f"match_count: {match_count}")
print(f"dataframe_columns: {dataframe_columns}")
# Validate Dropdown Data Structure safely (remove None, NaN, missing title, missing movie_id)
valid_rows = []
dropdown_options = []
for idx, row in raw_matches.iterrows():
title = row.get("title")
movie_id = row.get("movie_id")
year = row.get("year")
if pd.isna(title) or title is None or str(title).strip() == "":
continue
if pd.isna(movie_id) or movie_id is None:
continue
try:
movie_id = int(movie_id)
except Exception:
continue
try:
year_val = int(year) if not pd.isna(year) and year is not None else 0
year_str = f" ({year_val})" if year_val > 0 else ""
except Exception:
year_str = ""
choice_str = f"{title}{year_str} | ID: {movie_id}"
dropdown_options.append(choice_str)
valid_rows.append((title, year_str, movie_id, choice_str))
print(f"dropdown_options: {dropdown_options}")
if not valid_rows:
print("No matching movies found.")
return (
gr.update(choices=[], value=None, label="Select Matching Movie (No matching movies found.)"),
"<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>No matching movies found.</div>",
go.Figure(),
""
)
# Determine Match Type
match_type, matched_info = check_search_match_type(query, valid_rows)
print(f"Determined match_type: {match_type}")
if match_type in ("EXACT_MATCH", "ONE_MATCH"):
# Auto-select movie
title, year_str, movie_id, choice_str = matched_info
selected_movie_id = movie_id
selected_movie = raw_matches[raw_matches["movie_id"] == movie_id].iloc[0].to_dict()
print(f"Auto-selecting movie: {title}")
print(f"selected_movie_id: {selected_movie_id}")
print(f"selected_movie: {selected_movie}")
# Similarity Pipeline Validation
try:
sims_df = recommender.get_similar_movies(selected_movie_id, top_n=top_n)
grid_html = build_similar_grid_html(sims_df)
graph_fig = get_movie_relationship_graph(selected_movie, sims_df)
explanation_header = f"""
<div class="section-header" style="margin-top: 15px;">
<h3 class="section-title" style="font-size: 20px;">Because you watched "{selected_movie['title']}"...</h3>
<p class="section-desc">Latent vector embeddings from the DeepFM neural layers identify these as matching representations.</p>
</div>
"""
return (
gr.update(choices=dropdown_options, value=choice_str, label="Select Matching Movie"),
grid_html,
graph_fig,
explanation_header
)
except Exception as sim_err:
print(f"Similarity pipeline error for movie ID {selected_movie_id}: {sim_err}")
traceback.print_exc()
err_html = f"""
<div style='background-color: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); padding: 20px; border-radius: 8px; color: #FF6B6B;'>
<h4 style='margin-top:0;'>Similarity Pipeline Error</h4>
<p>Failed to find similar movies for <strong>{selected_movie['title']}</strong>.</p>
<pre style='background: rgba(0,0,0,0.2); padding: 10px; border-radius: 4px; overflow-x: auto; font-size:12px;'>{traceback.format_exc()}</pre>
</div>
"""
return (
gr.update(choices=dropdown_options, value=choice_str, label="Select Matching Movie"),
err_html,
go.Figure(),
""
)
else:
# MULTIPLE_MATCHES
print(f"Multiple matches found. Showing selector with {len(dropdown_options)} options.")
return (
gr.update(choices=dropdown_options, value=dropdown_options[0], label="Select Matching Movie"),
"<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Multiple matches found. Select a movie and click Find Similar.</div>",
go.Figure(),
""
)
except Exception as e:
print(f"Exception in search change handler: {e}")
traceback.print_exc()
err_html = f"""
<div style='background-color: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); padding: 20px; border-radius: 8px; color: #FF6B6B;'>
<h4 style='margin-top:0;'>Search Handler Error</h4>
<pre style='background: rgba(0,0,0,0.2); padding: 10px; border-radius: 4px; overflow-x: auto; font-size:12px;'>{traceback.format_exc()}</pre>
</div>
"""
return gr.update(choices=[], value=None, label="Search Error"), err_html, go.Figure(), ""
def find_similar_movies(choice_str, top_n_str):
"""
Runs vector similarity against DeepFM latent embeddings for the selected movie.
"""
print(f"--- Find Similar Movies Clicked ---")
print(f"choice_str: {repr(choice_str)}")
print(f"top_n_str: {repr(top_n_str)}")
if not choice_str:
return (
"<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Please search and select a movie above.</div>",
go.Figure(),
""
)
try:
selected_movie_id = int(choice_str.split("| ID: ")[-1])
top_n = int(top_n_str)
except Exception as e:
print(f"Error parsing similar movie choice: {e}")
traceback.print_exc()
err_html = f"""
<div style='background-color: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); padding: 20px; border-radius: 8px; color: #FF6B6B;'>
<h4 style='margin-top:0;'>Parsing Error</h4>
<p>Failed to parse the selected movie ID.</p>
<pre style='background: rgba(0,0,0,0.2); padding: 10px; border-radius: 4px; overflow-x: auto; font-size:12px;'>{traceback.format_exc()}</pre>
</div>
"""
return err_html, go.Figure(), ""
ref_match = df_raw[df_raw["movie_id"] == selected_movie_id]
if ref_match.empty:
return (
f"<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Movie with ID {selected_movie_id} not found in catalog.</div>",
go.Figure(),
""
)
ref_movie = ref_match.iloc[0]
selected_movie = ref_movie.to_dict()
print(f"selected_movie_id: {selected_movie_id}")
print(f"selected_movie: {selected_movie}")
# Calculate similarities
try:
sims_df = recommender.get_similar_movies(selected_movie_id, top_n=top_n)
grid_html = build_similar_grid_html(sims_df)
graph_fig = get_movie_relationship_graph(ref_movie, sims_df)
explanation_header = f"""
<div class="section-header" style="margin-top: 15px;">
<h3 class="section-title" style="font-size: 20px;">Because you watched "{ref_movie['title']}"...</h3>
<p class="section-desc">Latent vector embeddings from the DeepFM neural layers identify these as matching representations.</p>
</div>
"""
return grid_html, graph_fig, explanation_header
except Exception as e:
print(f"Similar movies error: {e}", flush=True)
traceback.print_exc()
err_html = f"""
<div style='background-color: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); padding: 20px; border-radius: 8px; color: #FF6B6B;'>
<h4 style='margin-top:0;'>Similarity Pipeline Error</h4>
<p>Failed to find similar movies for <strong>{ref_movie['title']}</strong>.</p>
<pre style='background: rgba(0,0,0,0.2); padding: 10px; border-radius: 4px; overflow-x: auto; font-size:12px;'>{traceback.format_exc()}</pre>
</div>
"""
return err_html, go.Figure(), ""
# Build Gradio Blocks Interface
with gr.Blocks(title="CineMind AI - Premium Movie Discovery") as demo:
# Sticky Navigation Header
gr.HTML(NAVBAR_HTML)
# Hidden components for Svelte state management (rendered but styled as hidden)
state_filtered_df = gr.State(df_raw)
state_current_page = gr.State(0)
hidden_movie_select = gr.Textbox(elem_id="hidden_movie_select", elem_classes="hidden-textbox", visible=True)
# Home Section Target
gr.HTML('<div id="home" class="section-anchor"></div>')
# Cinematic Hero Section
hero_html = f"""
<div class="hero-container">
<div class="hero-subtitle">AI-Powered Movie Discovery Platform</div>
<h1 class="hero-title">Find Your Next Favorite Movie</h1>
<p class="hero-description">Discover movies using advanced recommendation intelligence powered by DeepFM.</p>
<div class="hero-buttons">
<a href="#explore" class="btn primary">Explore Catalog</a>
<a href="#trending" class="btn secondary">Browse Trending</a>
</div>
</div>
"""
gr.HTML(hero_html)
# Movie Details Section Target (Global scroll target for clicks)
gr.HTML('<div id="movie-details-anchor" class="section-anchor"></div>')
# Tabs Navigation
with gr.Tabs(elem_classes="tabs"):
# TAB 1: 🎬 Recommendations
with gr.Tab("🎬 Recommendations"):
gr.HTML("""
<div class="section-header">
<h2 class="section-title">DeepFM Personalized Recommendations</h2>
<p class="section-desc">Select a user profile and configure recommendation size to activate active neural predictions.</p>
</div>
""")
# Control Inputs for Recommendations
with gr.Row(elem_classes="filter-row"):
user_id_dropdown = gr.Dropdown(
choices=["6", "7", "8", "10", "33", "42", "59", "79", "83", "87", "94", "97", "116", "131", "158", "164"],
value="6",
label="Select User Profile ID",
interactive=True
)
recommendation_count = gr.Dropdown(
choices=["5", "10", "20", "50"],
value="10",
label="Recommendation Count",
interactive=True
)
btn_rec_generate = gr.Button("Generate Recommendations", variant="primary", elem_classes="primary-btn")
# Explanation indicator
recs_mode_banner = gr.HTML(value="")
# Recommendation output grid
recs_grid_output = gr.HTML(value="<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Select a profile above and click 'Generate Recommendations' to load predicted content.</div>")
# Recommendation analytics
gr.HTML("""
<div class="section-header" style="margin-top: 30px;">
<h3 class="section-title" style="font-size: 18px;">Recommendation Analytics</h3>
</div>
""")
recs_metrics_output = gr.HTML(value="")
with gr.Row():
recs_chart_output = gr.Plot(label="Score Distribution")
# TAB 2: 🔍 Similar Movies (Section 3 Integration)
with gr.Tab("🔍 Similar Movies"):
gr.HTML("""
<div class="section-header">
<h2 class="section-title">Similar Content Finder</h2>
<p class="section-desc">Find related titles inspired by your favorite movies using DeepFM latent representation spaces.</p>
</div>
""")
with gr.Row(elem_classes="filter-row"):
with gr.Column(scale=3):
similar_search = gr.Textbox(
placeholder="Type movie name to search (e.g. Dinosaur, Inception, Lord)...",
label="Search Movie Title",
interactive=True
)
with gr.Column(scale=2):
similar_select = gr.Dropdown(
choices=[],
label="Select Matching Movie",
interactive=True
)
with gr.Column(scale=1):
similar_count = gr.Dropdown(
choices=["5", "10", "15", "20"],
value="10",
label="Find Count",
interactive=True
)
btn_similar_find = gr.Button("Find Similar", variant="primary", elem_classes="primary-btn")
# Dynamic header display
similar_ref_banner = gr.HTML(value="")
# Similar movies output grid
similar_grid_output = gr.HTML(value="<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Search and select a movie above, then click Find Similar.</div>")
# Plotly relationship graph
gr.HTML("""
<div class="section-header" style="margin-top: 30px;">
<h3 class="section-title" style="font-size: 18px;">Movie Relationship Explorer</h3>
</div>
""")
similar_graph_output = gr.Plot(label="Relationship Network")
# TAB 3: ℹ️ About
with gr.Tab("ℹ️ About"):
about_html = f"""
<div style="background-color: var(--surface-color); padding: 30px; border-radius: 16px; line-height: 1.6;">
<h2 class="section-title" style="margin-bottom: 15px;">About CineMind AI</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px; font-size: 15px;">
CineMind AI is a premium movie discovery platform powered by a production-grade <strong>DeepFM (Deep Factorization Machine)</strong> recommendation engine.
By combining the strength of factorization machines for low-order feature interactions with deep neural networks for high-order interactions,
the engine predicts user preferences with exceptional accuracy.
</p>
<h3 class="section-title" style="font-size: 18px; margin-bottom: 12px; margin-top: 20px;">Dataset Statistics</h3>
<div class="about-metrics" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0;">
<div style="background-color: var(--card-color); padding: 20px; border-radius: 10px; text-align: center;">
<div style="font-size: 32px; font-weight: 800; color: var(--accent-color);">{kpis['total_movies']:,}</div>
<div style="font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Movies Cataloged</div>
</div>
<div style="background-color: var(--card-color); padding: 20px; border-radius: 10px; text-align: center;">
<div style="font-size: 32px; font-weight: 800; color: var(--accent-color);">340,955</div>
<div style="font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Implicit Profiles</div>
</div>
<div style="background-color: var(--card-color); padding: 20px; border-radius: 10px; text-align: center;">
<div style="font-size: 32px; font-weight: 800; color: var(--accent-color);">23,105,815</div>
<div style="font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Interaction Signals</div>
</div>
</div>
</div>
"""
gr.HTML(about_html)
# TAB 4: 📊 Diagnostics
with gr.Tab("📊 Diagnostics"):
btn_refresh_diag = gr.Button("Refresh System Diagnostics", variant="secondary", elem_classes="secondary-btn")
diagnostics_panel = gr.HTML(value=get_diagnostics_html(recommender, poster_service))
# Section 4 Movie Insights panel & timeline charts
gr.HTML("""
<div style="margin-top: 40px;">
<h2 class="section-title">Selected Movie Insights</h2>
<p class="section-desc">Click any card in the Explorer Catalog or Trending lists to load deep analytics, consensus deviation, and timelines.</p>
</div>
""")
with gr.Column(elem_classes="stats-dashboard"):
details_panel = gr.HTML(value=build_movie_details_html(None))
with gr.Row():
single_movie_dist_plot = gr.Plot(label="Star Rating Distribution")
single_movie_time_plot = gr.Plot(label="Cumulative Votes Growth")
# Explore Section Target
gr.HTML('<div id="explore" class="section-anchor"></div>')
# Explore/Catalog Section
gr.HTML("""
<div class="section-header">
<h2 class="section-title">Explore Movie Catalog</h2>
<p class="section-desc">Search keywords or apply filters to discover movies.</p>
</div>
""")
# Search and Filter Form (Section 1 Refinements)
with gr.Row(elem_classes="filter-row"):
with gr.Column(scale=3):
search_box = gr.Textbox(
placeholder="Search by movie title or keywords...",
label="Search Movies",
max_lines=1,
interactive=True
)
with gr.Column(scale=2):
sort_dropdown = gr.Dropdown(
choices=["Highest Rated", "Most Popular", "Newest", "Oldest", "Alphabetical"],
value="Most Popular",
label="Sort By",
interactive=True
)
with gr.Row(elem_classes="filter-row"):
with gr.Column(scale=1):
year_min_slider = gr.Slider(
minimum=min_year,
maximum=max_year,
value=min_year,
step=1,
label="Start Year",
interactive=True
)
with gr.Column(scale=1):
year_max_slider = gr.Slider(
minimum=min_year,
maximum=max_year,
value=max_year,
step=1,
label="End Year",
interactive=True
)
with gr.Column(scale=1):
rating_slider = gr.Slider(
minimum=0.0,
maximum=5.0,
value=0.0,
step=0.5,
label="Minimum Average Rating",
interactive=True
)
with gr.Column(scale=1):
votes_slider = gr.Slider(
minimum=0,
maximum=10000,
value=0,
step=100,
label="Minimum Vote Count",
interactive=True
)
# Interactive Catalog Grid
init_filtered, _ = filter_and_sort_movies(df_raw, "", [min_year, max_year], 0.0, 0, "Most Popular")
init_sliced, init_total_pages = paginate_dataframe(init_filtered, 0, PAGE_SIZE)
catalog_grid = gr.HTML(build_movie_grid_html(init_sliced))
# Pagination Row
with gr.Row(elem_classes="pagination-container"):
btn_prev = gr.Button("← Previous", variant="secondary", interactive=False)
page_indicator = gr.HTML(f"<div class='page-indicator'>{get_page_label(0, init_total_pages)}</div>")
btn_next = gr.Button("Next →", variant="secondary", interactive=init_total_pages > 1)
# Trending Section Target
gr.HTML('<div id="trending" class="section-anchor"></div>')
# Trending Section
gr.HTML("""
<div class="section-header" style="margin-top: 40px;">
<h2 class="section-title">Trending Discoveries</h2>
<p class="section-desc">The top 20 movies trending right now, calculated using weighted user interaction metrics.</p>
</div>
""")
gr.HTML(build_movie_grid_html(trending_df))
# Statistics Section Target
gr.HTML('<div id="statistics" class="section-anchor"></div>')
# Statistics Dashboard
gr.HTML("""
<div class="section-header" style="margin-top: 40px;">
<h2 class="section-title">Catalog Statistics & Analytics</h2>
<p class="section-desc">Live metrics audit detailing ratings distributions and historical catalog release dates.</p>
</div>
""")
with gr.Column(elem_classes="stats-dashboard"):
# KPI widgets
gr.HTML(f"""
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value accent">{kpis['total_movies']:,}</div>
<div class="stat-label">Total Titles</div>
</div>
<div class="stat-item">
<div class="stat-value">{kpis['avg_rating']:.2f} ★</div>
<div class="stat-label">Average Rating</div>
</div>
<div class="stat-item">
<div class="stat-value">{kpis['avg_votes']:,}</div>
<div class="stat-label">Average Votes</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" style="font-size: 18px; font-weight: 700; white-space: normal; height: auto;">{kpis['most_popular']}</div>
<div class="stat-label" style="margin-top: 8px;">Most Popular Title</div>
</div>
<div class="stat-item">
<div class="stat-value" style="font-size: 18px; font-weight: 700; white-space: normal; height: auto;">{kpis['newest_movie']}</div>
<div class="stat-label" style="margin-top: 8px;">Newest Release</div>
</div>
</div>
""")
# Distribution charts
with gr.Row():
chart_rating = gr.Plot(get_rating_distribution_chart(df_raw))
chart_year = gr.Plot(get_year_distribution_chart(df_raw))
# Sticky Footer
gr.HTML(FOOTER_HTML)
# Bind callbacks
# Bind Filter inputs (dual year sliders) to catalog grid updates
filter_inputs = [search_box, year_min_slider, year_max_slider, rating_slider, votes_slider, sort_dropdown]
filter_outputs = [state_filtered_df, state_current_page, catalog_grid, page_indicator, btn_prev, btn_next]
# Bind change listeners
search_box.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
year_min_slider.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
year_max_slider.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
rating_slider.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
votes_slider.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
sort_dropdown.change(fn=filter_catalog, inputs=filter_inputs, outputs=filter_outputs)
# Pagination button clicks
btn_prev.click(
fn=change_page,
inputs=[gr.State(-1), state_current_page, state_filtered_df],
outputs=[state_current_page, catalog_grid, page_indicator, btn_prev, btn_next]
)
btn_next.click(
fn=change_page,
inputs=[gr.State(1), state_current_page, state_filtered_df],
outputs=[state_current_page, catalog_grid, page_indicator, btn_prev, btn_next]
)
# Selection details bindings (Wired to details panel HTML and Plotly graphs)
hidden_movie_select.change(
fn=select_movie,
inputs=hidden_movie_select,
outputs=[details_panel, single_movie_dist_plot, single_movie_time_plot]
)
# Section 2: DeepFM Recommendations trigger bindings
btn_rec_generate.click(
fn=generate_recommendations,
inputs=[user_id_dropdown, recommendation_count],
outputs=[recs_grid_output, recs_metrics_output, recs_chart_output, recs_mode_banner]
)
# Section 3: Similar Movies autocomplete & find trigger bindings
similar_search.change(
fn=handle_similar_search_change,
inputs=[similar_search, similar_count],
outputs=[similar_select, similar_grid_output, similar_graph_output, similar_ref_banner]
)
btn_similar_find.click(
fn=find_similar_movies,
inputs=[similar_select, similar_count],
outputs=[similar_grid_output, similar_graph_output, similar_ref_banner]
)
# Diagnostics refresh binding
btn_refresh_diag.click(
fn=lambda: get_diagnostics_html(recommender, poster_service),
inputs=[],
outputs=diagnostics_panel
)
if __name__ == "__main__":
demo.launch(css=CUSTOM_CSS, server_port=7860)