Spaces:
Sleeping
Sleeping
| 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) | |