Spaces:
Running
Running
import os | |
import threading | |
import time | |
import json | |
import requests | |
import pandas as pd | |
import numpy as np | |
from sklearn.feature_extraction.text import CountVectorizer | |
from sklearn.metrics.pairwise import cosine_similarity | |
from flask import Flask, request, jsonify | |
from flask_cors import CORS | |
import gradio as gr | |
from typing import List, Dict, Any, Optional | |
import html | |
import re | |
from dotenv import load_dotenv # Import load_dotenv | |
from pyngrok import ngrok | |
# --- Ngrok usage example (if needed) --- | |
NGROK_AUTH_TOKEN = os.getenv("Ngrok") | |
if NGROK_AUTH_TOKEN: | |
try: | |
ngrok.set_auth_token(NGROK_AUTH_TOKEN) | |
# Example: ngrok.connect(7860) | |
except ImportError: | |
print("pyngrok not installed; skipping ngrok setup.") | |
# --- MovieRecommendationSystem Class (from Cell C) --- | |
class MovieRecommendationSystem: | |
def __init__(self): | |
self.movies_df = None | |
self.similarity_matrix = None | |
self.vectorizer = CountVectorizer(stop_words='english') | |
# Load API key from environment variable | |
self.API_KEY = os.getenv("OMDB_API_KEY") | |
if not self.API_KEY: | |
print("π¨ WARNING: OMDB_API_KEY not found in environment variables.") | |
self.BASE_URL = "http://www.omdbapi.com/" | |
self.HEADERS = {} | |
def fetch_movie_by_title(self, title): | |
"""Fetch a single movie by title from OMDb API.""" | |
if not self.API_KEY: | |
print("OMDb API key is not set. Cannot fetch movie.") | |
return None | |
params = { | |
"apikey": self.API_KEY, | |
"t": title, | |
"plot": "full" | |
} | |
try: | |
response = requests.get(self.BASE_URL, headers=self.HEADERS, params=params, timeout=10) # Added timeout | |
if response.status_code == 200: | |
data = response.json() | |
if data.get("Response") == "True": | |
return data | |
print(f"Error fetching movie '{title}': {response.status_code} or movie not found") | |
return None | |
except requests.exceptions.Timeout: | |
print(f"Timeout fetching movie '{title}'.") | |
return None | |
except Exception as e: | |
print(f"Error fetching movie '{title}': {e}") | |
return None | |
def fetch_movies(self, titles=None, limit=400): | |
"""Fetch a list of movies, either from provided titles or a default list.""" | |
if titles is None: | |
titles = [ | |
"Inception", "The Dark Knight", "Interstellar", "The Matrix", "Fight Club", | |
"Pulp Fiction", "Forrest Gump", "The Shawshank Redemption", "Gladiator", "Titanic", | |
"Avatar", "The Avengers", "Jurassic Park", "Star Wars", "The Lord of the Rings", | |
"Harry Potter", "Pirates of the Caribbean", "The Godfather", "Back to the Future", | |
"Indiana Jones", "The Lion King", "Toy Story", "Finding Nemo", "Up", "WALL-E", | |
"The Incredibles", "Coco", "Spider-Man", "Iron Man", "Captain America", | |
"Thor", "Black Panther", "Deadpool", "Logan", "X-Men", "Batman Begins", | |
"The Dark Knight Rises", "Man of Steel", "Wonder Woman", "Aquaman", "Parasite", | |
"Joker", "Once Upon a Time in Hollywood", "Avengers: Endgame", "Toy Story 4", | |
"Spider-Man: Into the Spider-Verse", "Green Book", "Bohemian Rhapsody", | |
"A Star Is Born", "The Irishman", "1917", "Ford v Ferrari", "Little Women", | |
"Marriage Story", "Knives Out", "Us", "Midsommar", "Parasite", "Joker", | |
"Once Upon a Time in Hollywood", "Avengers: Endgame", "Toy Story 4", | |
"Spider-Man: Into the Spider-Verse", "Green Book", "Bohemian Rhapsody", | |
"A Star Is Born", "The Irishman", "1917", "Ford v Ferrari", "Little Women", | |
"Marriage Story", "Knives Out", "Us", "Midsommar", "Get Out", "Dunkirk", | |
"La La Land", "Moonlight", "Arrival", "Hacksaw Ridge", "Hell or High Water", | |
"Manchester by the Sea", "Hidden Figures", "Lion", "Fences", "Deadpool", | |
"Logan", "Arrival", "Hell or High Water", "Manchester by the Sea", "Hidden Figures", | |
"Lion", "Fences", "Zootopia", "Moana", "Sing Street", "The Nice Guys", | |
"Captain America: Civil War", "Doctor Strange", "Fantastic Beasts and Where to Find Them", | |
"Rogue One: A Star Wars Story", "Arrival", "Hacksaw Ridge", "Hell or High Water", | |
"Manchester by the Sea", "Hidden Figures", "Lion", "Fences", "Zootopia", | |
"Moana", "Sing Street", "The Nice Guys", "Captain America: Civil War", "Doctor Strange", | |
"Fantastic Beasts and Where to Find Them", "Rogue One: A Star Wars Story", "Deadpool", | |
"Logan", "Arrival", "Hell or High Water", "Manchester by the Sea", "Hidden Figures", | |
"Lion", "Fences", "Zootopia", "Moana", "Sing Street", "The Nice Guys", | |
"Captain America: Civil War", "Doctor Strange", "Fantastic Beasts and Where to Find Them", | |
"Rogue One: A Star Wars Story", "The Martian", "Mad Max: Fury Road", "Inside Out", | |
"Spotlight", "The Revenant", "Room", "Brooklyn", "Carol", "Sicario", | |
"Straight Outta Compton", "The Big Short", "Bridge of Spies", "Ex Machina", | |
"The Hateful Eight", "Anomalisa", "Son of Saul", "The Lobster", "Amy", | |
"Cartel Land", "Winter on Fire: Ukraine's Fight for Freedom", "What Happened, Miss Simone?", | |
"Listen to Me Marlon", "The Look of Silence", "Shaun the Sheep Movie", "When Marnie Was There", | |
"Boy and the World", "Mustang", "Embrace of the Serpent", "Theeb", "A War", | |
"A Bigger Splash", "Florence Foster Jenkins", "Hail, Caesar!", "Julieta", | |
"Love & Friendship", "Maggie's Plan", "Miles Ahead", "Our Little Sister", | |
"The Lobster", "Amy", "Cartel Land", "Winter on Fire: Ukraine's Fight for Freedom", | |
"What Happened, Miss Simone?", "Listen to Me Marlon", "The Look of Silence", | |
"Shaun the Sheep Movie", "When Marnie Was There", "Boy and the World", | |
"Mustang", "Embrace of the Serpent", "Theeb", "A War", "A Bigger Splash", | |
"Florence Foster Jenkins", "Hail, Caesar!", "Julieta", "Love & Friendship", | |
"Maggie's Plan", "Miles Ahead", "Our Little Sister", "Paterson", "Sing Street", | |
"The Nice Guys", "Captain America: Civil War", "Doctor Strange", | |
"Fantastic Beasts and Where to Find Them", "Rogue One: A Star Wars Story", | |
"The Martian", "Mad Max: Fury Road", "Inside Out", "Spotlight", "The Revenant", | |
"Room", "Brooklyn", "Carol", "Sicario", "Straight Outta Compton", | |
"The Big Short", "Bridge of Spies", "Ex Machina", "The Hateful Eight", | |
"Anomalisa", "Son of Saul", "The Lobster", "Amy", "Cartel Land", | |
"Winter on Fire: Ukraine's Fight for Freedom", "What Happened, Miss Simone?", | |
"Listen to Me Marlon", "The Look of Silence", "Shaun the Sheep Movie", | |
"When Marnie Was There", "Boy and the World", "Mustang", "Embrace of the Serpent", | |
"Theeb", "A War", "A Bigger Splash", "Florence Foster Jenkins", | |
"Hail, Caesar!", "Julieta", "Love & Friendship", "Maggie's Plan", | |
"Miles Ahead", "Our Little Sister", "Paterson", "Sing Street", "The Nice Guys", | |
"Captain America: Civil War", "Doctor Strange", | |
"Fantastic Beasts and Where to Find Them", "Rogue One: A Star Wars Story", | |
"The Martian", "Mad Max: Fury Road", "Inside Out", "Spotlight", "The Revenant", | |
"Room", "Brooklyn", "Carol", "Sicario", "Straight Outta Compton", | |
"The Big Short", "Bridge of Spies", "Ex Machina", "The Hateful Eight", | |
"Anomalisa", "Son of Saul", "The Lobster", "Amy", "Cartel Land", | |
"Winter on Fire: Ukraine's Fight for Freedom", "What Happened, Miss Simone?", | |
"Listen to Me Marlon", "The Look of Silence", "Shaun the Sheep Movie", | |
"When Marnie Was There", "Boy and the World", "Mustang", "Embrace of the Serpent", | |
"Theeb", "A War", "A Bigger Splash", "Florence Foster Jenkins", | |
"Hail, Caesar!", "Julieta", "Love & Friendship", "Maggie's Plan", | |
"Miles Ahead", "Our Little Sister", "Paterson" | |
][:limit] | |
movies = [] | |
titles_to_fetch = titles[:limit] if limit is not None else titles | |
for title in titles_to_fetch: | |
movie_data = self.fetch_movie_by_title(title) | |
if movie_data: | |
movies.append(movie_data) | |
return movies | |
def prepare_movie_data(self): | |
"""Prepare movie data from OMDb API or fallback if API fetch fails.""" | |
movies = self.fetch_movies() | |
if not movies: | |
print("π¨ API returned no movies. Loading fallback dataset.") | |
fallback_movies = [ | |
{'id': 'tt0372784', 'title': 'Batman Begins', 'overview': 'A young Bruce Wayne becomes Batman to fight crime in Gotham.', 'genres': 'Action, Adventure, Crime', 'cast': 'Christian Bale, Michael Caine', 'poster_path': 'https://m.media-amazon.com/images/M/MV5BMjE3NDcyNDExNF5BMl5BanBnXkFtZTcwMDYwNDk0OA@@._V1_SX300.jpg', 'vote_average': 8.2, 'release_date': '2005', 'combined_features': 'Action Adventure Crime Christian Bale Michael Caine A young Bruce Wayne becomes Batman to fight crime in Gotham.'}, | |
{'id': 'tt0468569', 'title': 'The Dark Knight', 'overview': 'Batman faces the Joker, a criminal mastermind.', 'genres': 'Action, Crime, Drama, Thriller', 'cast': 'Christian Bale, Heath Ledger', 'poster_path': 'https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_SX300.jpg', 'vote_average': 9.0, 'release_date': '2008', 'combined_features': 'Action Crime Drama Thriller Christian Bale Heath Ledger Batman faces the Joker, a criminal mastermind.'}, | |
{'id': 'tt1345836', 'title': 'The Dark Knight Rises', 'overview': 'Batman returns to save Gotham from Bane.', 'genres': 'Action, Crime, Thriller', 'cast': 'Christian Bale, Tom Hardy', 'poster_path': 'https://m.media-amazon.com/images/M/MV5BMTk4ODQzNDY3Ml5BMl5BanBnXkFtZTcwODA0NTM4Nw@@._V1_SX300.jpg', 'vote_average': 8.4, 'release_date': '2012', 'combined_features': 'Action Crime Thriller Christian Bale Tom Hardy Batman returns to save Gotham from Bane.'}, | |
{'id': 'tt0144084', 'title': 'American Psycho', 'overview': 'A Wall Street banker leads a double life as a serial killer.', 'genres': 'Crime, Drama, Horror', 'cast': 'Christian Bale, Willem Dafoe', 'poster_path': 'https://m.media-amazon.com/images/M/MV5BZTM2ZGJmNzktNzc3My00ZWMzLTg0MjItZjBlMWJiNDE0NjZiXkEyXkFqcGc@._V1_SX300.jpg', 'vote_average': 7.6, 'release_date': '2000', 'combined_features': 'Crime Drama Horror Christian Bale Willem Dafoe A Wall Street banker leads a double life as a serial killer.'}, | |
{'id': 'tt0246578', 'title': 'Donnie Darko', 'overview': 'A troubled teenager is plagued by visions of a man in a rabbit costume.', 'genres': 'Drama, Sci-Fi, Thriller', 'cast': 'Jake Gyllenhaal, Maggie Gyllenhaal', 'poster_path': 'https://m.media-amazon.com/images/M/MV5BZjZlZDlkYTktMmU1My00ZDBiLWE0TAQtNjkzZDFiYTY0ZmMyXkEyXkFqcGc@._V1_SX300.jpg', 'vote_average': 8.0, 'release_date': '2001', 'combined_features': 'Drama Sci-Fi Thriller Jake Gyllenhaal Maggie Gyllenhaal A troubled teenager is plagued by visions of a man in a rabbit costume.'} | |
] | |
self.movies_df = pd.DataFrame(fallback_movies) | |
else: | |
print(f"β Successfully fetched {len(movies)} movies from OMDb API.") | |
movie_data = [] | |
for movie in movies: | |
movie_info = { | |
'id': movie.get('imdbID', movie.get('Title', 'unknown')), | |
'title': movie.get('Title', ''), | |
'overview': movie.get('Plot', ''), | |
'genres': movie.get('Genre', ''), | |
'cast': movie.get('Actors', ''), | |
'poster_path': movie.get('Poster', ''), | |
'vote_average': float(movie.get('imdbRating', '0')) if movie.get('imdbRating') not in ['N/A', None] else 0, | |
'release_date': movie.get('Year', ''), | |
'combined_features': f"{movie.get('Genre', '')} {movie.get('Actors', '')} {movie.get('Plot', '')}" | |
} | |
movie_data.append(movie_info) | |
self.movies_df = pd.DataFrame(movie_data) | |
self.build_similarity_matrix() | |
return self.movies_df | |
def build_similarity_matrix(self): | |
"""Build similarity matrix for recommendations based on combined features.""" | |
if self.movies_df is not None and not self.movies_df.empty: | |
max_features = 5000 | |
self.vectorizer = CountVectorizer(stop_words='english', max_features=max_features) | |
corpus = self.movies_df['combined_features'].fillna('').tolist() | |
vectorized_features = self.vectorizer.fit_transform(corpus) | |
self.similarity_matrix = cosine_similarity(vectorized_features) | |
print(f"β Similarity matrix built with shape: {self.similarity_matrix.shape}") | |
else: | |
print("π¨ Cannot build similarity matrix: movies_df is empty.") | |
def get_recommendations(self, selected_movie_ids, num_recommendations=5): | |
"""Get movie recommendations based on selected movie IDs.""" | |
if self.similarity_matrix is None or self.movies_df.empty: | |
print("Debug: Similarity matrix or movies_df is empty.") | |
return [] | |
selected_indices = self.movies_df[self.movies_df['id'].isin(selected_movie_ids)].index.tolist() | |
if not selected_indices: | |
print("Debug: No selected movies found in DataFrame for recommendations.") | |
return [] | |
avg_similarity_scores = np.mean(self.similarity_matrix[selected_indices], axis=0) | |
movie_indices = np.argsort(avg_similarity_scores)[::-1] | |
recommendations = [] | |
for idx in movie_indices: | |
movie = self.movies_df.iloc[idx] | |
# Ensure the recommended movie is not one of the selected movies | |
if movie['id'] not in selected_movie_ids: | |
recommendations.append(movie.to_dict()) | |
if len(recommendations) >= num_recommendations: | |
break | |
return recommendations | |
# Initialize the recommender globally | |
recommender = MovieRecommendationSystem() | |
# --- Flask Application (from Cell D, modified) --- | |
app = Flask(__name__) | |
CORS(app) # Enable CORS | |
def index(): | |
"""Health check endpoint""" | |
return jsonify({ | |
"message": "Netflix Clone API is running!", | |
"status": "success", | |
"endpoints": ["/api/movies", "/api/recommend"] | |
}) | |
def get_movies(): | |
"""Get all movies for display""" | |
try: | |
if recommender.movies_df is None or recommender.movies_df.empty: | |
print("Preparing movie data...") | |
recommender.prepare_movie_data() | |
print(f"Loaded {len(recommender.movies_df)} movies") | |
movies = recommender.movies_df.to_dict('records') | |
return jsonify(movies) | |
except Exception as e: | |
print(f"Error in get_movies: {e}") | |
return jsonify({'error': 'Failed to fetch movies'}), 500 | |
def recommend_movies(): | |
"""Get recommendations based on selected movies""" | |
try: | |
data = request.json | |
selected_movie_ids = data.get('selected_movies', []) | |
if len(selected_movie_ids) < 5: | |
return jsonify({'error': 'Please select at least 5 movies'}), 400 | |
print(f"Getting recommendations for movies: {selected_movie_ids}") | |
recommendations = recommender.get_recommendations(selected_movie_ids) | |
return jsonify(recommendations) | |
except Exception as e: | |
print(f"Error in recommend_movies: {e}") | |
return jsonify({'error': 'Failed to get recommendations'}), 500 | |
def health_check(): | |
"""Health check endpoint""" | |
return jsonify({ | |
"status": "healthy", | |
"movies_loaded": len(recommender.movies_df) if recommender.movies_df is not None else 0, | |
"similarity_matrix_built": recommender.similarity_matrix is not None | |
}) | |
# Function to start Flask server (from Cell E, modified) | |
def start_flask_server(): | |
"""Start Flask server in background""" | |
try: | |
print("π Starting Flask backend server...") | |
# Run Flask app on port 5000, accessible locally within the Space | |
app.run(host='127.0.0.1', port=5000, debug=False) | |
except Exception as e: | |
print(f"β Error starting Flask server: {e}") | |
# --- Gradio Application (from Cell 7P4A_qIhjvbT, modified) --- | |
# API_BASE_URL now points to the Flask app running locally within the Space | |
API_BASE_URL = "http://127.0.0.1:5000" | |
MAX_SELECTIONS = 10 | |
MIN_RECOMMENDATIONS = 5 | |
class CinemaCloneApp: | |
def __init__(self): | |
self.selected_movies = [] | |
self.all_movies = [] | |
self.recommendations = [] | |
def sanitize_input(self, text: str) -> str: | |
"""Sanitize user input to prevent XSS attacks""" | |
if not isinstance(text, str): | |
return "" | |
text = re.sub(r'<[^>]*>', '', text) | |
text = html.escape(text) | |
return text.strip() | |
def validate_movie_data(self, movie: Dict[str, Any]) -> bool: | |
"""Validate movie data structure""" | |
required_fields = ['id', 'title'] | |
return all(field in movie and movie[field] for field in required_fields) | |
def fetch_movies_from_backend(self) -> List[Dict[str, Any]]: | |
"""Fetch movies from the Flask backend with comprehensive error handling""" | |
try: | |
response = requests.get( | |
f"{API_BASE_URL}/api/movies", | |
timeout=60, # Increased timeout | |
headers={'Accept': 'application/json'} | |
) | |
if response.status_code == 200: | |
content_type = response.headers.get('content-type', '') | |
if 'application/json' not in content_type: | |
# Attempt to read text response for debugging non-JSON errors | |
print(f"Warning: Received non-JSON response (status {response.status_code}). Content: {response.text[:500]}...") # Print first 500 chars | |
raise ValueError(f"Backend returned non-JSON response (Status: {response.status_code})") | |
movies = response.json() | |
if isinstance(movies, list) and len(movies) > 0: | |
validated_movies = [] | |
for movie in movies: | |
if self.validate_movie_data(movie): | |
movie['title'] = self.sanitize_input(movie.get('title', '')) | |
movie['overview'] = self.sanitize_input(movie.get('overview', '')) | |
movie['genres'] = self.sanitize_input(movie.get('genres', '')) | |
movie['cast'] = self.sanitize_input(movie.get('cast', '')) # Sanitize cast as well | |
validated_movies.append(movie) | |
self.all_movies = validated_movies | |
print(f"Successfully fetched and validated {len(validated_movies)} movies from backend.") | |
return validated_movies | |
elif isinstance(movies, list) and len(movies) == 0: | |
print("Backend returned an empty movie list.") | |
return self.get_fallback_movies() | |
else: | |
print(f"Backend returned unexpected data format: {movies}") | |
raise ValueError("Invalid movie data structure from backend") | |
else: | |
try: | |
error_response = response.json() | |
print(f"Backend error response (Status {response.status_code}): {error_response}") | |
except json.JSONDecodeError: | |
print(f"Backend non-JSON error response (Status {response.status_code}) from recommendations endpoint: {response.text[:500]}...") | |
raise requests.RequestException(f"Backend request failed with status: {response.status_code}") | |
except requests.exceptions.Timeout: | |
print(f"Timeout fetching movies from backend at {API_BASE_URL}/api/movies") | |
return self.get_fallback_movies() | |
except requests.exceptions.ConnectionError as ce: | |
print(f"Connection error fetching movies from backend at {API_BASE_URL}/api/movies: {ce}") | |
print("Ensure the Flask backend is running and accessible at http://127.0.0.1:5000.") | |
return self.get_fallback_movies() | |
except Exception as e: | |
print(f"An unexpected error occurred fetching movies from backend: {e}") | |
return self.get_fallback_movies() | |
def get_recommendations_from_backend(self, selected_ids: List[str]) -> List[Dict[str, Any]]: | |
"""Get recommendations from Flask backend with security validation""" | |
try: | |
if not selected_ids or not isinstance(selected_ids, list): | |
raise ValueError("Invalid selected movie IDs") | |
sanitized_ids = [self.sanitize_input(str(id_)) for id_ in selected_ids if id_] | |
response = requests.post( | |
f"{API_BASE_URL}/api/recommend", | |
json={"selected_movies": sanitized_ids}, | |
headers={'Content-Type': 'application/json', 'Accept': 'application/json'}, | |
timeout=30 | |
) | |
if response.status_code == 200: | |
content_type = response.headers.get('content-type', '') | |
if 'application/json' not in content_type: | |
print(f"Warning: Received non-JSON response (status {response.status_code}) from recommendations endpoint. Content: {response.text[:500]}...") | |
raise ValueError(f"Backend returned non-JSON response (Status: {response.status_code}) from recommendations endpoint") | |
recommendations = response.json() | |
if isinstance(recommendations, list): | |
validated_recs = [] | |
for rec in recommendations: | |
if self.validate_movie_data(rec): | |
rec['title'] = self.sanitize_input(rec.get('title', '')) | |
rec['overview'] = self.sanitize_input(rec.get('overview', '')) | |
rec['genres'] = self.sanitize_input(rec.get('genres', '')) | |
rec['cast'] = self.sanitize_input(rec.get('cast', '')) # Sanitize cast as well | |
validated_recs.append(rec) | |
print(f"Successfully received and validated {len(validated_recs)} recommendations.") | |
return validated_recs | |
else: | |
print(f"Backend returned unexpected data format for recommendations: {recommendations}") | |
raise ValueError("Invalid recommendations data structure from backend") | |
else: | |
try: | |
error_response = response.json() | |
print(f"Backend error response (Status {response.status_code}) from recommendations endpoint: {error_response}") | |
except json.JSONDecodeError: | |
print(f"Backend non-JSON error response (Status {response.status_code}) from recommendations endpoint: {response.text[:500]}...") | |
raise requests.RequestException(f"Backend recommendation request failed with status: {response.status_code}") | |
except requests.exceptions.Timeout: | |
print(f"Timeout getting recommendations from backend at {API_BASE_URL}/api/recommend") | |
return [] | |
except requests.exceptions.ConnectionError as ce: | |
print(f"Connection error getting recommendations from backend at {API_BASE_URL}/api/recommend: {ce}") | |
print("Ensure the Flask backend is running and accessible at http://127.0.0.1:5000.") | |
return [] | |
except Exception as e: | |
print(f"An unexpected error occurred getting recommendations: {e}") | |
return [] | |
def create_movie_card_html(self, movie: Dict[str, Any], is_selected: bool = False, is_recommendation: bool = False) -> str: | |
"""Create HTML for a movie card with React-inspired styling and animations""" | |
# Ensure all fields are present with default empty strings to avoid KeyError | |
title = html.escape(movie.get('title', 'Unknown')) | |
overview = html.escape(movie.get('overview', '')[:200] + "..." if len(movie.get('overview', '')) > 200 else movie.get('overview', '')) | |
genres = html.escape(movie.get('genres', '')) | |
cast = html.escape(movie.get('cast', '')[:150] + "..." if len(movie.get('cast', '')) > 150 else movie.get('cast', '')) | |
rating = float(movie.get('vote_average', 0)) | |
year = html.escape(str(movie.get('release_date', ''))) | |
movie_id = html.escape(str(movie.get('id', ''))) # Ensure ID is also sanitized and present | |
poster_url = movie.get('poster_path', '') | |
if not poster_url or not poster_url.startswith(('http://', 'https://')): | |
poster_url = 'https://via.placeholder.com/300x450/1a1a1a/fff?text=No+Image' | |
selected_class = "selected" if is_selected else "" | |
rec_class = "recommendation" if is_recommendation else "" | |
selection_indicator = "β" if is_selected else "+" | |
genre_list = genres.split(', ') if genres else [] | |
genre_tags_html = "" | |
for genre in genre_list[:3]: | |
genre_tags_html += f'<span class="genre-tag">{html.escape(genre.strip())}</span>' # Sanitize genre tags | |
# Use data-movie-id for JavaScript interaction | |
return f""" | |
<div class="movie-card {selected_class} {rec_class}" data-movie-id="{movie_id}" onclick="selectMovieByTitle('{title}')"> | |
<div class="movie-poster-container"> | |
<img src="{html.escape(poster_url)}" | |
alt="{title}" | |
class="movie-poster" | |
onerror="this.src='https://via.placeholder.com/300x450/1a1a1a/fff?text=No+Image'"> | |
<div class="movie-overlay"> | |
<div class="action-buttons"> | |
<!-- Add your action buttons here if needed --> | |
</div> | |
</div> | |
<div class="selection-indicator">{selection_indicator}</div> | |
</div> | |
<div class="movie-info"> | |
<h3 class="movie-title">{title}</h3> | |
<div class="movie-meta"> | |
<div class="movie-rating"> | |
<span class="star">β</span> | |
<span class="rating-value">{rating:.1f}</span> | |
</div> | |
<div class="movie-year">{year}</div> | |
</div> | |
<div class="genre-tags"> | |
{genre_tags_html} | |
</div> | |
<div class="movie-cast"> | |
<strong>Cast:</strong> {cast} | |
</div> | |
<div class="movie-overview">{overview}</div> | |
</div> | |
</div> | |
""" | |
def create_movies_grid_html(self, movies: List[Dict[str, Any]], is_recommendation: bool = False) -> str: | |
"""Create HTML grid of movie cards with React-inspired layout""" | |
if not movies: | |
return f""" | |
<div class="no-movies"> | |
<div class="no-movies-icon">π¬</div> | |
<h3>No {'recommendations' if is_recommendation else 'movies'} available</h3> | |
<p>{'Select more movies to get better recommendations' if is_recommendation else 'Click Load Movies to start exploring'}</p> | |
</div> | |
""" | |
cards_html = "" | |
for movie in movies[:500]: # Limit for performance, can be adjusted | |
# Find the full movie object from self.all_movies to check selection status | |
full_movie_data = next((item for item in self.all_movies if item.get('id') == movie.get('id')), None) | |
is_selected = full_movie_data and full_movie_data.get('id') in self.selected_movies | |
cards_html += self.create_movie_card_html(movie, is_selected, is_recommendation) | |
grid_class = "recommendations-grid" if is_recommendation else "movies-grid" | |
return f""" | |
<div class="{grid_class}"> | |
{cards_html} | |
</div> | |
""" | |
# Initialize the app instance globally | |
gradio_app_instance = CinemaCloneApp() | |
# Enhanced CSS with React-inspired styling and animations | |
cinema_css = """ | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); | |
* { | |
box-sizing: border-box; | |
} | |
.gradio-container { | |
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0f0f0f 100%); | |
color: white; | |
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
min-height: 100vh; | |
} | |
.cinema-header { | |
text-align: center; | |
padding: 40px 20px; | |
background: linear-gradient(135deg, #e50914 0%, #ff6b6b 50%, #8b5cf6 100%); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
font-size: clamp(2.5rem, 5vw, 4rem); | |
font-weight: 900; | |
letter-spacing: -3px; | |
margin-bottom: 20px; | |
text-shadow: 0 0 30px rgba(229, 9, 20, 0.3); | |
} | |
.subtitle { | |
font-size: 1.2rem; | |
opacity: 0.8; | |
margin-bottom: 40px; | |
font-weight: 400; | |
} | |
.selection-counter { | |
background: linear-gradient(135deg, #e50914 0%, #ff6b6b 100%); | |
padding: 20px 30px; | |
border-radius: 50px; | |
text-align: center; | |
font-weight: 700; | |
margin: 30px auto; | |
max-width: 400px; | |
box-shadow: 0 15px 35px rgba(229, 9, 20, 0.4); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
font-size: 1.1rem; | |
} | |
.movies-grid, .recommendations-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | |
gap: 30px; | |
padding: 30px; | |
max-height: 800px; | |
overflow-y: auto; | |
scrollbar-width: thin; | |
scrollbar-color: #e50914 #1a1a1a; | |
} | |
.movies-grid::-webkit-scrollbar, .recommendations-grid::-webkit-scrollbar { | |
width: 8px; | |
} | |
.movies-grid::-webkit-scrollbar-track, .recommendations-grid::-webkit-scrollbar-track { | |
background: #1a1a1a; | |
border-radius: 4px; | |
} | |
.movies-grid::-webkit-scrollbar-thumb, .recommendations-grid::-webkit-scrollbar-thumb { | |
background: linear-gradient(135deg, #e50914, #ff6b6b); | |
border-radius: 4px; | |
} | |
.movie-card { | |
position: relative; | |
border-radius: 20px; | |
overflow: hidden; | |
background: linear-gradient(145deg, #1e1e1e, #2a2a2a); | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
cursor: pointer; | |
border: 2px solid transparent; | |
backdrop-filter: blur(10px); | |
} | |
.movie-card:hover { | |
transform: scale(1.05) translateY(-15px); | |
box-shadow: 0 25px 50px rgba(229, 9, 20, 0.4); | |
border-color: rgba(229, 9, 20, 0.5); | |
} | |
.movie-card.selected { | |
border-color: #e50914; | |
box-shadow: 0 0 30px rgba(229, 9, 20, 0.6); | |
transform: scale(1.02); | |
} | |
.movie-card.recommendation { | |
background: linear-gradient(145deg, #2a1a2a, #3a2a3a); | |
border-color: rgba(139, 92, 246, 0.5); | |
} | |
.movie-card.recommendation:hover { | |
box-shadow: 0 25px 50px rgba(139, 92, 246, 0.4); | |
border-color: #8b5cf6; | |
} | |
.movie-poster-container { | |
position: relative; | |
width: 100%; | |
height: 400px; | |
overflow: hidden; | |
} | |
.movie-poster { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
transition: transform 0.4s ease; | |
} | |
.movie-card:hover .movie-poster { | |
transform: scale(1.1); | |
} | |
.movie-overlay { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: linear-gradient( | |
to bottom, | |
transparent 0%, | |
transparent 40%, | |
rgba(0, 0, 0, 0.7) 70%, | |
rgba(0, 0, 0, 0.9) 100% | |
); | |
display: flex; | |
align-items: flex-end; | |
justify-content: center; | |
padding: 20px; | |
opacity: 0; | |
transition: opacity 0.3s ease; | |
} | |
.movie-card:hover .movie-overlay { | |
opacity: 1; | |
} | |
.action-buttons { | |
display: flex; | |
gap: 12px; | |
transform: translateY(20px); | |
transition: transform 0.3s ease; | |
} | |
.movie-card:hover .action-buttons { | |
transform: translateY(0); | |
} | |
.action-btn { | |
width: 45px; | |
height: 45px; | |
border-radius: 50%; | |
border: 2px solid rgba(255, 255, 255, 0.3); | |
background: rgba(255, 255, 255, 0.1); | |
color: white; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
backdrop-filter: blur(10px); | |
} | |
.action-btn.primary { | |
background: linear-gradient(135deg, #e50914, #ff6b6b); | |
border-color: #e50914; | |
} | |
.action-btn:hover { | |
transform: scale(1.1); | |
background: rgba(255, 255, 255, 0.2); | |
} | |
.action-btn.primary:hover { | |
background: linear-gradient(135deg, #ff1a25, #ff7b7b); | |
} | |
.selection-indicator { | |
position: absolute; | |
top: 15px; | |
right: 15px; | |
width: 35px; | |
height: 35px; | |
border-radius: 50%; | |
background: rgba(229, 9, 20, 0.9); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
font-weight: bold; | |
font-size: 18px; | |
backdrop-filter: blur(10px); | |
border: 2px solid rgba(255, 255, 255, 0.2); | |
transition: all 0.3s ease; | |
} | |
.movie-card.selected .selection-indicator { | |
background: linear-gradient(135deg, #e50914, #ff6b6b); | |
transform: scale(1.1); | |
} | |
.movie-info { | |
padding: 25px; | |
background: linear-gradient(135deg, #1a1a1a, #2a2a2a); | |
} | |
.movie-title { | |
font-size: 1.3rem; | |
font-weight: 700; | |
margin-bottom: 12px; | |
color: white; | |
line-height: 1.3; | |
display: -webkit-box; | |
-webkit-line-clamp: 2; | |
-webkit-box-orient: vertical; | |
overflow: hidden; | |
} | |
.movie-meta { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 15px; | |
} | |
.movie-rating { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
font-weight: 600; | |
} | |
.star { | |
font-size: 1.2rem; | |
} | |
.rating-value { | |
color: #ffd700; | |
font-size: 1rem; | |
} | |
.movie-year { | |
color: #999; | |
font-size: 0.9rem; | |
font-weight: 500; | |
background: rgba(255, 255, 255, 0.1); | |
padding: 4px 12px; | |
border-radius: 20px; | |
} | |
.genre-tags { | |
display: flex; | |
gap: 8px; | |
flex-wrap: wrap; | |
margin-bottom: 15px; | |
} | |
.genre-tag { | |
background: linear-gradient(135deg, #e50914, #ff6b6b); | |
padding: 4px 12px; | |
border-radius: 20px; | |
font-size: 0.75rem; | |
font-weight: 600; | |
color: white; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.movie-card.recommendation .genre-tag { | |
background: linear-gradient(135deg, #8b5cf6, #a78bfa); | |
} | |
.movie-cast { | |
color: #ccc; | |
font-size: 0.85rem; | |
margin-bottom: 12px; | |
line-height: 1.4; | |
} | |
.movie-cast strong { | |
color: #e50914; | |
font-weight: 600; | |
} | |
.movie-overview { | |
color: #ddd; | |
font-size: 0.8rem; | |
line-height: 1.5; | |
display: -webkit-box; | |
-webkit-line-clamp: 4; | |
-webkit-box-orient: vertical; | |
overflow: hidden; | |
} | |
.no-movies { | |
text-align: center; | |
color: #ccc; | |
padding: 80px 40px; | |
background: rgba(255, 255, 255, 0.02); | |
border-radius: 20px; | |
margin: 40px; | |
border: 2px dashed rgba(255, 255, 255, 0.1); | |
} | |
.no-movies-icon { | |
font-size: 4rem; | |
margin-bottom: 20px; | |
opacity: 0.5; | |
} | |
.no-movies h3 { | |
font-size: 1.5rem; | |
margin-bottom: 10px; | |
color: white; | |
} | |
.no-movies p { | |
font-size: 1rem; | |
opacity: 0.7; | |
} | |
.recommendations-section { | |
margin-top: 60px; | |
padding: 40px; | |
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(229, 9, 20, 0.1)); | |
border-radius: 30px; | |
border: 1px solid rgba(139, 92, 246, 0.2); | |
backdrop-filter: blur(20px); | |
} | |
.section-title { | |
font-size: 2.5rem; | |
font-weight: 800; | |
margin-bottom: 30px; | |
background: linear-gradient(135deg, #8b5cf6, #e50914); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
text-align: center; | |
} | |
.error-message { | |
background: linear-gradient(135deg, rgba(229, 9, 20, 0.2), rgba(255, 107, 107, 0.1)); | |
color: #ff6b6b; | |
padding: 20px; | |
border-radius: 15px; | |
text-align: center; | |
margin: 20px 0; | |
border: 1px solid rgba(229, 9, 20, 0.3); | |
backdrop-filter: blur(10px); | |
} | |
.success-message { | |
background: linear-gradient(135deg, rgba(76, 175, 80, 0.2), rgba(139, 195, 74, 0.1)); | |
color: #4caf50; | |
padding: 20px; | |
border-radius: 15px; | |
text-align: center; | |
margin: 20px 0; | |
border: 1px solid rgba(76, 175, 80, 0.3); | |
backdrop-filter: blur(10px); | |
} | |
/* Button Styling */ | |
.gr-button { | |
background: linear-gradient(135deg, #e50914, #ff6b6b) !important; | |
border: none !important; | |
color: white !important; | |
font-weight: 700 !important; | |
border-radius: 25px !important; | |
padding: 15px 30px !important; | |
transition: all 0.3s ease !important; | |
box-shadow: 0 10px 25px rgba(229, 9, 20, 0.3) !important; | |
} | |
.gr-button:hover { | |
transform: translateY(-3px) !important; | |
box-shadow: 0 15px 35px rgba(229, 9, 20, 0.5) !important; | |
background: linear-gradient(135deg, #ff1a25, #ff7b7b) !important; | |
} | |
.gr-dropdown { | |
background: rgba(255, 255, 255, 0.1) !important; | |
border: 1px solid rgba(229, 9, 20, 0.3) !important; | |
color: white !important; | |
border-radius: 15px !important; | |
backdrop-filter: blur(10px) !important; | |
} | |
/* Responsive Design */ | |
@media (max-width: 768px) { | |
.movies-grid, .recommendations-grid { | |
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
gap: 20px; | |
padding: 20px; | |
} | |
.cinema-header { | |
font-size: 2.5rem; | |
padding: 30px 15px; | |
} | |
.movie-card { | |
border-radius: 15px; | |
} | |
.movie-poster-container { | |
height: 350px; | |
} | |
} | |
/* Animation Keyframes */ | |
@keyframes fadeInUp { | |
from { | |
opacity: 0; | |
transform: translateY(30px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.movie-card { | |
animation: fadeInUp 0.6s ease-out; | |
} | |
.movie-card:nth-child(even) { | |
animation-delay: 0.1s; | |
} | |
.movie-card:nth-child(3n) { | |
animation-delay: 0.2s; | |
} | |
</style> | |
<script> | |
// This script is needed for the Gradio component to interact with the HTML cards | |
// It triggers the Gradio 'select_btn' click event with the movie title | |
function selectMovieByTitle(title) { | |
// Find the Gradio Dropdown element by its label or other identifier | |
// This might need adjustment based on Gradio's internal structure | |
const dropdown = document.querySelector('.gr-dropdown label').parentElement.querySelector('select'); | |
if (dropdown) { | |
// Set the value of the dropdown to the clicked movie title | |
dropdown.value = title; | |
// Find the 'Add/Remove Selection' button | |
const selectButton = document.querySelector('button.gr-button').nextElementSibling; // Assuming it's the button after Load | |
// Find the select button more reliably | |
const buttons = document.querySelectorAll('button.gr-button'); | |
let selectButtonElement = null; | |
for (const btn of buttons) { | |
if (btn.textContent.includes('Add/Remove Selection')) { // Match button text | |
selectButtonElement = btn; | |
break; | |
} | |
} | |
if (selectButtonElement) { | |
// Trigger a click event on the select button | |
selectButtonElement.click(); | |
console.log('Triggered select button for:', title); | |
} else { | |
console.error("Could not find the 'Add/Remove Selection' button."); | |
} | |
} else { | |
console.error("Could not find the movie dropdown element."); | |
} | |
} | |
</script> | |
""" | |
def load_movies(): | |
"""Load movies from backend with enhanced UI feedback""" | |
try: | |
movies = gradio_app_instance.fetch_movies_from_backend() | |
movies_html = gradio_app_instance.create_movies_grid_html(movies, is_recommendation=False) | |
status = f"<div class='success-message'>β¨ Successfully loaded {len(movies)} amazing movies!</div>" | |
movie_choices = ["π¬ Select a movie"] + [movie['title'] for movie in movies if movie.get('title')] | |
# Clear selected movies on load | |
gradio_app_instance.selected_movies = [] | |
selection_counter_html = f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
return movies_html, status_display, gr.update(visible=False), gr.update(choices=movie_choices, value="π¬ Select a movie"), "", selection_counter_html | |
except Exception as e: | |
error_msg = f"<div class='error-message'>β Oops! Failed to load movies: {str(e)}</div>" | |
return "<div class='error-message'>Failed to load movies. Please try again.</div>", error_msg, gr.update(visible=False), gr.update(choices=["π¬ Select a movie"], value="π¬ Select a movie"), "", "<div class='selection-counter'>Selected: 0/10</div>" | |
def toggle_movie_selection(movie_title: str): | |
"""Toggle movie selection with enhanced feedback""" | |
movie_title = gradio_app_instance.sanitize_input(movie_title) | |
if not movie_title or movie_title == "π¬ Select a movie": | |
return gr.update(), "<div class='error-message'>Please select a movie first! π¬</div>", gr.update(visible=False), f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
selected_movie = None | |
for movie in gradio_app_instance.all_movies: | |
if movie.get('title') == movie_title: | |
selected_movie = movie | |
break | |
if not selected_movie: | |
return gr.update(), "<div class='error-message'>β Movie not found in our collection</div>", gr.update(visible=False), f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
movie_id = selected_movie['id'] | |
if movie_id in gradio_app_instance.selected_movies: | |
gradio_app_instance.selected_movies.remove(movie_id) | |
action = "removed from" | |
emoji = "β" | |
else: | |
if len(gradio_app_instance.selected_movies) >= MAX_SELECTIONS: | |
return gr.update(), f"<div class='error-message'>π« Maximum {MAX_SELECTIONS} movies can be selected</div>", gr.update(visible=False), f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
gradio_app_instance.selected_movies.append(movie_id) | |
action = "added to" | |
emoji = "β" | |
# Re-render the movies grid to update selection indicators | |
movies_html = gradio_app_instance.create_movies_grid_html(gradio_app_instance.all_movies, is_recommendation=False) | |
status = f"<div class='success-message'>{emoji} '{movie_title}' {action} your collection</div>" | |
selection_counter_html = f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
show_rec_btn = len(gradio_app_instance.selected_movies) >= MIN_RECOMMENDATIONS | |
return movies_html, status_display, gr.update(visible=show_rec_btn), selection_counter_html | |
def get_recommendations(): | |
"""Get movie recommendations with beautiful presentation""" | |
if len(gradio_app_instance.selected_movies) < MIN_RECOMMENDATIONS: | |
return gr.update(), f"<div class='error-message'>π― Please select at least {MIN_RECOMMENDATIONS} movies to get personalized recommendations</div>", gr.update(visible=False) | |
try: | |
recommendations = gradio_app_instance.get_recommendations_from_backend(gradio_app_instance.selected_movies) | |
if not recommendations: | |
return gr.update(), "<div class='error-message'>π€ No recommendations found. Try selecting different movies!</div>", gr.update(visible=False) | |
rec_html = f""" | |
<div class="recommendations-section"> | |
<div class="section-title">π― Curated Just For You</div> | |
{gradio_app_instance.create_movies_grid_html(recommendations, is_recommendation=True)} | |
</div> | |
""" | |
status = f"<div class='success-message'>π Found {len(recommendations)} perfect recommendations based on your {len(gradio_app_instance.selected_movies)} selections!</div>" | |
return rec_html, status_display, gr.update(visible=True) | |
except Exception as e: | |
error_msg = f"<div class='error-message'>β Error getting recommendations: {str(e)}</div>" | |
return gr.update(), error_msg, gr.update(visible=False) | |
def clear_selections(): | |
"""Clear all selections with confirmation""" | |
gradio_app_instance.selected_movies.clear() | |
# Re-render the movies grid to clear selection indicators | |
movies_html = gradio_app_instance.create_movies_grid_html(gradio_app_instance.all_movies, is_recommendation=False) | |
selection_counter_html = f"<div class='selection-counter'>Selected: {len(gradio_app_instance.selected_movies)}/{MAX_SELECTIONS}</div>" | |
return movies_html, "<div class='success-message'>π All selections cleared! Start fresh with new choices</div>", gr.update(visible=False), gr.update(visible=False), gr.update(value="π¬ Select a movie"), "", selection_counter_html | |
def search_movies(query: str): | |
"""Search movies by title and update the grid""" | |
query = gradio_app_instance.sanitize_input(query).lower() | |
if not query: | |
# If search query is empty, display all movies | |
return gradio_app_instance.create_movies_grid_html(gradio_app_instance.all_movies, is_recommendation=False) | |
else: | |
# Filter movies based on the query | |
filtered_movies = [ | |
movie for movie in gradio_app_instance.all_movies | |
if query in movie.get('title', '').lower() | |
] | |
return gradio_app_instance.create_movies_grid_html(filtered_movies, is_recommendation=False) | |
# Create the stunning Gradio interface | |
with gr.Blocks(css=cinema_css, title="CinemaAI - Movie Recommendations", theme=gr.themes.Default()) as demo: | |
gr.HTML(""" | |
<div class="cinema-header"> | |
π¬ CINEMA AI | |
</div> | |
<div style="text-align: center; margin-bottom: 40px;"> | |
<h2 class="subtitle">Discover Your Next Cinematic Adventure</h2> | |
<p style="opacity: 0.7; font-size: 1.1rem;">Select your favorite movies and let our AI curate personalized recommendations just for you</p> | |
</div> | |
""") | |
with gr.Row(): | |
with gr.Column(scale=3): | |
load_btn = gr.Button("π¬ Load Movie Collection", variant="primary", size="lg") | |
with gr.Column(scale=2): | |
clear_btn = gr.Button("π Clear All Selections", variant="secondary") | |
selection_counter_display = gr.HTML("<div class='selection-counter'>Selected: 0/10</div>") | |
status_display = gr.HTML("<div class='selection-counter'>π Click 'Load Movie Collection' to begin your journey</div>") | |
with gr.Row(): | |
movie_dropdown = gr.Dropdown( | |
choices=["π¬ Select a movie"], | |
label="π― Choose Your Favorite Movie", | |
value="π¬ Select a movie", | |
interactive=True | |
) | |
select_btn = gr.Button("β¨ Add/Remove Selection", variant="secondary") | |
search_bar = gr.Textbox(label="π Search for a movie by title", placeholder="e.g., Inception", interactive=True) | |
movies_display = gr.HTML("<div class='no-movies'><div class='no-movies-icon'>π¬</div><h3>Your Movie Collection Awaits</h3><p>Load movies to start exploring amazing cinema</p></div>") | |
rec_btn = gr.Button("π― Get My Personal Recommendations", variant="primary", size="lg", visible=False) | |
recommendations_display = gr.HTML("", visible=False) | |
# Event handlers | |
load_btn.click( | |
fn=load_movies, | |
outputs=[movies_display, status_display, recommendations_display, movie_dropdown, search_bar, selection_counter_display] | |
) | |
select_btn.click( | |
fn=toggle_movie_selection, | |
inputs=[movie_dropdown], | |
outputs=[movies_display, status_display, rec_btn, selection_counter_display] | |
) | |
rec_btn.click( | |
fn=get_recommendations, | |
outputs=[recommendations_display, status_display, recommendations_display] | |
) | |
clear_btn.click( | |
fn=clear_selections, | |
outputs=[movies_display, status_display, rec_btn, recommendations_display, movie_dropdown, search_bar, selection_counter_display] | |
) | |
search_bar.change( | |
fn=search_movies, | |
inputs=[search_bar], | |
outputs=[movies_display] | |
) | |
# --- Main execution block --- | |
if __name__ == "__main__": | |
load_dotenv() # Load environment variables from .env file | |
# Start Flask server in a separate thread | |
flask_thread = threading.Thread(target=start_flask_server) | |
flask_thread.daemon = True | |
flask_thread.start() | |
# Give Flask a moment to start | |
time.sleep(5) # Increased sleep time | |
# Launch Gradio interface | |
demo.launch() | |