import os import ast import gradio as gr from supabase import create_client from dotenv import load_dotenv import numpy as np from sentence_transformers import SentenceTransformer import logging from functools import lru_cache import time import requests load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Supabase client supabase = create_client( os.getenv("SUPABASE_URL", ""), os.getenv("SUPABASE_KEY", "") ) # Load model model = None try: model = SentenceTransformer("all-mpnet-base-v2") logger.info("āœ… Model loaded successfully on HF Spaces") except Exception as e: logger.error(f"āŒ Model loading failed: {e}") def get_embedding(text: str): if model is None: raise Exception("Model not loaded") return model.encode(text).tolist() def cosine_similarity(a, b): a = np.array(a, dtype=float) b = np.array(b, dtype=float) norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) if norm_a == 0 or norm_b == 0: return 0.0 return np.dot(a, b) / (norm_a * norm_b) @lru_cache(maxsize=1000) def cached_embedding(text: str): return tuple(get_embedding(text)) def get_multiple_recommendations(user_input: str, count: int = 3, exclude_titles: list = None): if exclude_titles is None: exclude_titles = [] input_embedding = list(cached_embedding(user_input)) try: search_result = supabase.rpc('match_movies', { 'query_embedding': input_embedding, 'match_threshold': 0.01, 'match_count': count * 3 }).execute() if search_result.data and len(search_result.data) > 0: filtered_results = [ movie for movie in search_result.data if movie['title'].lower() not in [title.lower() for title in exclude_titles] ] logger.info(f"Vector search found {len(search_result.data)} results") return filtered_results[:count] except Exception as e: logger.warning(f"Vector search failed: {e}") # Fallback to manual search try: response = supabase.table("movies").select("id,title,overview,embedding,vote_average,release_date").limit(2000).execute() movies = response.data except Exception as e: logger.error(f"Database fetch error: {e}") return [] if not movies: return [] movie_scores = [] for movie in movies: if movie['title'].lower() in [title.lower() for title in exclude_titles]: continue emb = movie.get('embedding') if emb: if isinstance(emb, str): try: emb = ast.literal_eval(emb) except (ValueError, SyntaxError): continue try: similarity = cosine_similarity(input_embedding, emb) movie_scores.append({ **movie, 'similarity': similarity }) except Exception as e: continue movie_scores.sort(key=lambda x: x['similarity'], reverse=True) return movie_scores[:count] # Core recommendation function def process_movie_query(user_input: str, multiple: bool = False, count: int = 3): """Main function that processes movie queries""" start_time = time.time() if not user_input.strip(): return { "reply": "Please provide input text describing what kind of movie you'd like.", "movies": [], "processing_time": 0 } logger.info(f"šŸŽ¬ Processing query: '{user_input}' (multiple: {multiple})") if multiple: try: recommendations = get_multiple_recommendations(user_input, count, []) processing_time = time.time() - start_time if recommendations: reply = f"šŸŽÆ Here are {len(recommendations)} recommendations for '{user_input}':\\n\\n" movies_list = [] for i, movie in enumerate(recommendations, 1): title = movie['title'] overview = movie['overview'] rating = movie.get('vote_average', 'N/A') year = movie.get('release_date', '')[:4] if movie.get('release_date') else 'Unknown' similarity = movie.get('similarity', 0) reply += f"{i}. **{title}** ({year}) - {rating}/10\\n" reply += f" {overview[:100]}{'...' if len(overview) > 100 else ''}\\n" reply += f" Match: {similarity:.1%}\\n\\n" movies_list.append({ "title": title, "overview": overview, "rating": rating, "year": year, "similarity": similarity }) return { "reply": reply, "movies": movies_list, "count": len(recommendations), "processing_time": processing_time } else: return { "reply": "Sorry, I couldn't find good movie matches for your request. Try describing what genre, mood, or themes you're interested in!", "movies": [], "processing_time": processing_time } except Exception as e: logger.error(f"Multiple recommendations error: {e}") return { "reply": f"Error processing your request: {str(e)}", "movies": [], "processing_time": time.time() - start_time } # Single recommendation logic try: input_embedding = list(cached_embedding(user_input)) except Exception as e: logger.error(f"Embedding generation error: {e}") return { "reply": f"Error processing your request: {str(e)}", "movie": "", "processing_time": time.time() - start_time } try: search_result = supabase.rpc('match_movies', { 'query_embedding': input_embedding, 'match_threshold': 0.1, 'match_count': 5 }).execute() if search_result.data: best_movie = search_result.data[0] best_score = best_movie.get('similarity', 0) else: raise Exception("No vector search results") except Exception as e: logger.warning(f"Vector search failed, using manual search: {e}") try: response = supabase.table("movies").select("id,title,overview,embedding,vote_average,release_date").limit(1000).execute() movies = response.data except Exception as e: logger.error(f"Database fetch error: {e}") return { "reply": f"Error fetching movies from database: {str(e)}", "movie": "", "processing_time": time.time() - start_time } if not movies: return { "reply": "No movies found in the database.", "movie": "", "processing_time": time.time() - start_time } best_score = -1 best_movie = None for movie in movies: emb = movie.get('embedding') if emb: if isinstance(emb, str): try: emb = ast.literal_eval(emb) except (ValueError, SyntaxError): continue try: similarity = cosine_similarity(input_embedding, emb) if similarity > best_score: best_score = similarity best_movie = movie except Exception as e: continue processing_time = time.time() - start_time if best_movie and best_score > 0.1: title = best_movie['title'] overview = best_movie['overview'] rating = best_movie.get('vote_average', 'N/A') year = best_movie.get('release_date', '')[:4] if best_movie.get('release_date') else 'Unknown' reply = f"šŸŽ¬ I recommend '{title}' ({year})" if rating != 'N/A': reply += f" - Rating: {rating}/10" reply += f"\\n\\nšŸ“– Overview: {overview}" reply += f"\\n\\nšŸŽÆ Match confidence: {best_score:.1%}" return { "reply": reply, "movie": title, "confidence": best_score, "processing_time": processing_time, "details": { "title": title, "overview": overview, "rating": rating, "year": year } } else: return { "reply": "Sorry, I couldn't find a good movie match for your request. Try describing what genre, mood, or themes you're interested in!", "movie": "", "confidence": best_score if best_movie else 0, "processing_time": processing_time } # āœ… Gradio API functions that return JSON for your frontend def api_query_single(user_input): """API function for single recommendations - returns JSON string""" result = process_movie_query(user_input, multiple=False) import json return json.dumps(result) def api_query_multiple(user_input, count=3): """API function for multiple recommendations - returns JSON string""" result = process_movie_query(user_input, multiple=True, count=int(count)) import json return json.dumps(result) def api_health(): """API health check - returns JSON string""" try: response = supabase.table("movies").select("id", count="exact").execute() movie_count = response.count result = { "status": "healthy", "model": "all-mpnet-base-v2" if model else "not loaded", "total_movies": movie_count, "platform": "HF Spaces" } import json return json.dumps(result) except Exception as e: import json return json.dumps({"status": "error", "error": str(e)}) def api_stats(): """API stats - returns JSON string""" try: response = supabase.table("movies").select("id", count="exact").execute() movie_count = response.count result = { "total_movies": movie_count, "model": "all-mpnet-base-v2" if model else "not loaded", "embedding_dimension": "768", "platform": "HF Spaces" } import json return json.dumps(result) except Exception as e: import json return json.dumps({"error": str(e)}) # Gradio functions for the interface def gradio_single(user_input): if not user_input or not user_input.strip(): return "Please provide a movie description!" result = process_movie_query(user_input, multiple=False) return result.get("reply", "Error occurred") def gradio_multiple(user_input, count=3): if not user_input or not user_input.strip(): return "Please provide a movie description!" result = process_movie_query(user_input, multiple=True, count=int(count)) return result.get("reply", "Error occurred") def gradio_health(): try: response = supabase.table("movies").select("id", count="exact").execute() movie_count = response.count return f"āœ… Status: Healthy\n🧠 Model: {'Loaded' if model else 'Not Loaded'}\nšŸŽ¬ Movies: {movie_count:,}\nšŸ  Platform: HF Spaces\nšŸ”— API: Available via Gradio endpoints" except Exception as e: return f"āŒ Error: {str(e)}" # Create Gradio interface with API endpoints with gr.Blocks(title="šŸŽ¬ Movie Recommender", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # šŸŽ¬ AI Movie Recommender **Intelligent movie recommendations powered by sentence transformers** āœ… **Model**: all-mpnet-base-v2 loaded successfully šŸ“Š **Database**: 9,672+ movies with embeddings šŸš€ **Status**: Ready for recommendations! šŸ”— **API**: Available via Gradio predict endpoints """) with gr.Tab("šŸŽ¬ Single Recommendation"): with gr.Row(): with gr.Column(): single_input = gr.Textbox( label="Describe your perfect movie", placeholder="romantic comedy with witty dialogue", lines=3 ) single_btn = gr.Button("šŸŽ¬ Get Recommendation", variant="primary") with gr.Column(): single_output = gr.Textbox( label="Your Movie Recommendation", lines=10, show_copy_button=True ) gr.Examples( examples=[ ["romantic comedy"], ["action movie with explosions"], ["sci-fi space adventure"], ["psychological thriller"], ["feel-good family film"] ], inputs=single_input ) with gr.Tab("šŸ“‹ Multiple Recommendations"): with gr.Row(): with gr.Column(): multi_input = gr.Textbox( label="Describe what you're looking for", placeholder="movies like Inception", lines=3 ) count_slider = gr.Slider(2, 5, value=3, step=1, label="Number of recommendations") multi_btn = gr.Button("šŸ“‹ Get Multiple", variant="primary") with gr.Column(): multi_output = gr.Textbox( label="Your Movie Recommendations", lines=15, show_copy_button=True ) with gr.Tab("šŸ”§ System Status"): health_btn = gr.Button("šŸ„ Run Health Check", variant="secondary") health_output = gr.Textbox(label="System Status", lines=6) with gr.Tab("šŸ”— API Reference"): gr.Markdown(""" ## šŸ“” API Endpoints for Frontend Integration Your React frontend can call these Gradio API endpoints: **Base URL**: `https://Oshuboi-movie-recommender-api.hf.space` ### Single Recommendation: ```javascript fetch(API_URL + "/api/predict", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ data: ["romantic comedy"], fn_index: 0 // Single recommendation function }) }) ``` ### Multiple Recommendations: ```javascript fetch(API_URL + "/api/predict", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ data: ["action movies", 3], // [query, count] fn_index: 1 // Multiple recommendation function }) }) ``` ### Health Check: ```javascript fetch(API_URL + "/api/predict", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ data: [], fn_index: 2 // Health check function }) }) ``` ### Stats: ```javascript fetch(API_URL + "/api/predict", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ data: [], fn_index: 3 // Stats function }) }) ``` """) # Event handlers for Gradio interface single_btn.click(gradio_single, inputs=single_input, outputs=single_output) multi_btn.click(gradio_multiple, inputs=[multi_input, count_slider], outputs=multi_output) health_btn.click(gradio_health, outputs=health_output) if __name__ == "__main__": logger.info("šŸš€ Starting Movie Recommender with Gradio API") # Check database connection try: stats_response = supabase.table("movies").select("id", count="exact").execute() logger.info(f"āœ… Database connected! Found {stats_response.count} movies") except Exception as e: logger.error(f"āš ļø Database connection issue: {e}") # Launch Gradio with API access demo.launch( server_name="0.0.0.0", server_port=7860, show_api=True, # This exposes /api/predict endpoints share=False )