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'
' f'Results for "{q}"' f'
' ) elif mt == "FRANCHISE_MATCH": msg = ( f'
' f'Found {count} related titles for "{q}"' f'
' ) elif mt == "PARTIAL_MATCH": msg = ( f'
' f'"{q}" ' f'is not available. Showing related titles instead.' f'
' ) elif mt == "NO_MATCH": msg = ( f'
' f'No matches found for "{q}"' f'
' ) # 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"
Error generating recommendations: {e}
", "
Metrics unavailable
", go.Figure(), "
Error occurred
" ) # Calculate metrics metrics = recommender.get_recommendation_analytics(recs_df) # Render outputs grid_html = build_recommendations_grid_html(recs_df) metrics_html = f"""
{metrics['avg_confidence']:.1f}%
Model Confidence
{metrics['diversity_score']:.1f}%
Diversity Index
{metrics['personalization_score']:.1f}%
Personalization Fit
""" # 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"""
Recommendation Mode: {rec_type_badge}
This profile is built dynamically by combining factorization low-order mappings with high-order neural net predictions.
""" 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 = "
Search and select a movie above, then click Find Similar.
" 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.)"), "
No matching movies found.
", 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"""

Because you watched "{selected_movie['title']}"...

Latent vector embeddings from the DeepFM neural layers identify these as matching representations.

""" 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"""

Similarity Pipeline Error

Failed to find similar movies for {selected_movie['title']}.

{traceback.format_exc()}
""" 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"), "
Multiple matches found. Select a movie and click Find Similar.
", go.Figure(), "" ) except Exception as e: print(f"Exception in search change handler: {e}") traceback.print_exc() err_html = f"""

Search Handler Error

{traceback.format_exc()}
""" 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 ( "
Please search and select a movie above.
", 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"""

Parsing Error

Failed to parse the selected movie ID.

{traceback.format_exc()}
""" return err_html, go.Figure(), "" ref_match = df_raw[df_raw["movie_id"] == selected_movie_id] if ref_match.empty: return ( f"
Movie with ID {selected_movie_id} not found in catalog.
", 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"""

Because you watched "{ref_movie['title']}"...

Latent vector embeddings from the DeepFM neural layers identify these as matching representations.

""" 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"""

Similarity Pipeline Error

Failed to find similar movies for {ref_movie['title']}.

{traceback.format_exc()}
""" 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('
') # Cinematic Hero Section hero_html = f"""
AI-Powered Movie Discovery Platform

Find Your Next Favorite Movie

Discover movies using advanced recommendation intelligence powered by DeepFM.

Explore Catalog Browse Trending
""" gr.HTML(hero_html) # Movie Details Section Target (Global scroll target for clicks) gr.HTML('
') # Tabs Navigation with gr.Tabs(elem_classes="tabs"): # TAB 1: đŸŽŦ Recommendations with gr.Tab("đŸŽŦ Recommendations"): gr.HTML("""

DeepFM Personalized Recommendations

Select a user profile and configure recommendation size to activate active neural predictions.

""") # 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="
Select a profile above and click 'Generate Recommendations' to load predicted content.
") # Recommendation analytics gr.HTML("""

Recommendation Analytics

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

Similar Content Finder

Find related titles inspired by your favorite movies using DeepFM latent representation spaces.

""") 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="
Search and select a movie above, then click Find Similar.
") # Plotly relationship graph gr.HTML("""

Movie Relationship Explorer

""") similar_graph_output = gr.Plot(label="Relationship Network") # TAB 3: â„šī¸ About with gr.Tab("â„šī¸ About"): about_html = f"""

About CineMind AI

CineMind AI is a premium movie discovery platform powered by a production-grade DeepFM (Deep Factorization Machine) 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.

Dataset Statistics

{kpis['total_movies']:,}
Movies Cataloged
340,955
Implicit Profiles
23,105,815
Interaction Signals
""" 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("""

Selected Movie Insights

Click any card in the Explorer Catalog or Trending lists to load deep analytics, consensus deviation, and timelines.

""") 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('
') # Explore/Catalog Section gr.HTML("""

Explore Movie Catalog

Search keywords or apply filters to discover movies.

""") # 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"
{get_page_label(0, init_total_pages)}
") btn_next = gr.Button("Next →", variant="secondary", interactive=init_total_pages > 1) # Trending Section Target gr.HTML('') # Trending Section gr.HTML("""

Trending Discoveries

The top 20 movies trending right now, calculated using weighted user interaction metrics.

""") gr.HTML(build_movie_grid_html(trending_df)) # Statistics Section Target gr.HTML('
') # Statistics Dashboard gr.HTML("""

Catalog Statistics & Analytics

Live metrics audit detailing ratings distributions and historical catalog release dates.

""") with gr.Column(elem_classes="stats-dashboard"): # KPI widgets gr.HTML(f"""
{kpis['total_movies']:,}
Total Titles
{kpis['avg_rating']:.2f} ★
Average Rating
{kpis['avg_votes']:,}
Average Votes
{kpis['most_popular']}
Most Popular Title
{kpis['newest_movie']}
Newest Release
""") # 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)