Spaces:
Sleeping
Sleeping
| import sys, os, json, gradio as gr | |
| from pathlib import Path | |
| # Assuming get_recommendations functions will be updated to accept parsed_query_tags | |
| from retrieval.retrieve_books_50000 import get_recommendations as book_recs | |
| from retrieval.retrieve_movies_50000 import get_recommendations as movie_recs | |
| from utils.query_parser import parse_user_query # Import the enhanced parser | |
| from typing import List, Dict, Any, Optional, Tuple | |
| # --- Path Configuration --- | |
| # Get the root directory of the project, assuming this script is in `interface/` | |
| ROOT = Path(__file__).parent.parent.resolve() # Go up one level from 'interface' | |
| os.chdir(ROOT) # Change current working directory to ROOT | |
| sys.path.append(str(ROOT)) # Add ROOT to sys.path for module imports | |
| # --- No more explicit preloading here. Rely on retrieval modules to load data. --- | |
| # --- Recommendation Function --- | |
| def smart_recommend(query: str, choice: str, top_k: int, mood_boost: bool) -> str: | |
| """ | |
| Provides smart book or movie recommendations based on user query and preferences. | |
| Args: | |
| query (str): The user's natural language query. | |
| choice (str): User's preferred media type ("Books" or "Movies") from radio button. | |
| top_k (int): Number of top recommendations to retrieve. | |
| mood_boost (bool): Whether to use SBERT (mood-aware) or TF-IDF (keyword-based) for retrieval. | |
| Returns: | |
| str: HTML formatted string of recommendations or an error message. | |
| """ | |
| if not query.strip(): | |
| return "❗ Please enter a query to get recommendations." | |
| # Parse the user's query to extract structured tags | |
| parsed_query_tags = parse_user_query(query) | |
| # Determine the actual media type to recommend based on query preference or radio button | |
| media_type_to_recommend = parsed_query_tags["media_type_preference"] | |
| if not media_type_to_recommend: # If no explicit preference in query, use radio button choice | |
| # Convert "Books" to "book", "Movies" to "movie" | |
| media_type_to_recommend = choice.lower().rstrip('s') | |
| recs: List[Dict[str, Any]] = [] | |
| retrieval_method = "sbert" if mood_boost else "tfidf" | |
| # --- Retrieve Recommendations based on parsed query and determined media type --- | |
| if media_type_to_recommend == "book": | |
| # print(f"Retrieving books with query: '{query}', parsed tags: {parsed_query_tags}") | |
| # Pass the full parsed_query_tags dictionary to the retrieval function | |
| books = book_recs(query, top_k, retrieval_method, parsed_query_tags) | |
| for b in books: | |
| b["media"] = "book" | |
| # --- MODIFICATION 3: Capitalize first letter of each author's name --- | |
| if "authors" in b and isinstance(b["authors"], list): | |
| b["authors"] = [author.title() for author in b["authors"]] # .title() capitalizes first letter of each word | |
| # --- END MODIFICATION 3 --- | |
| recs.extend(books) | |
| elif media_type_to_recommend == "movie": | |
| # print(f"Retrieving movies with query: '{query}', parsed tags: {parsed_query_tags}") | |
| # Pass the full parsed_query_tags dictionary to the retrieval function | |
| movies = movie_recs(query, top_k, retrieval_method, parsed_query_tags) | |
| for m in movies: | |
| m["media"] = "movie" | |
| recs.extend(movies) | |
| else: | |
| # This case should ideally not be reached if choice is always "Books" or "Movies" | |
| return "⚠️ Please select a valid recommendation type (Books or Movies)." | |
| if not recs: | |
| # Provide more specific feedback if no recommendations are found | |
| msg = f"⚠️ No {media_type_to_recommend} recommendations found for your query." | |
| # Add details from parsed tags for better user feedback | |
| if parsed_query_tags["genres"]: | |
| msg += f" (Genres: {', '.join(parsed_query_tags['genres'])})" | |
| if parsed_query_tags["mood"]: | |
| msg += f" (Moods: {', '.join(parsed_query_tags['mood'])})" | |
| if parsed_query_tags["target_audience"]: | |
| msg += f" (Audience: {parsed_query_tags['target_audience'].replace('_', ' ')})" | |
| if parsed_query_tags["era"] or parsed_query_tags["decade"]: | |
| msg += f" (Time: {parsed_query_tags['era'] or ''}{' ' if parsed_query_tags['era'] and parsed_query_tags['decade'] else ''}{parsed_query_tags['decade'] or ''})" | |
| if parsed_query_tags["specific_person"]: | |
| msg += f" (By: {parsed_query_tags['specific_person']})" | |
| return msg.strip() | |
| # --- Construct HTML Output --- | |
| html_output = "" | |
| # Display Parsed Tags for user feedback | |
| parsed_tags_html = "<h4>Extracted Query Tags:</h4>" | |
| parsed_tags_html += "<ul>" | |
| parsed_tags_html += f"<li><strong>Genres:</strong> {', '.join(parsed_query_tags['genres']) if parsed_query_tags['genres'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Moods:</strong> {', '.join(parsed_query_tags['mood']) if parsed_query_tags['mood'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Audience:</strong> {parsed_query_tags['target_audience'].replace('_', ' ') if parsed_query_tags['target_audience'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Era:</strong> {parsed_query_tags['era'] if parsed_query_tags['era'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Decade:</strong> {parsed_query_tags['decade'] if parsed_query_tags['decade'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Specific Person (Director/Author):</strong> {parsed_query_tags['specific_person'] if parsed_query_tags['specific_person'] else 'None'}</li>" | |
| parsed_tags_html += f"<li><strong>Explicit Media Preference:</strong> {parsed_query_tags['media_type_preference'] if parsed_query_tags['media_type_preference'] else 'None'}</li>" | |
| parsed_tags_html += "</ul><hr/>" | |
| html_output += parsed_tags_html | |
| for it in recs: | |
| html_output += '<div style="border:1px solid #ccc; border-radius:8px; padding:10px; margin:10px 0; display:flex; align-items:flex-start;">' # Align items to start | |
| # Cover image width | |
| img_url = it.get("cover_url") if it["media"] == "book" else it.get("poster_url") | |
| if img_url: | |
| html_output += f'<img src="{img_url}" style="width:150px; border-radius:4px; margin-right:15px; flex-shrink: 0;" />' # flex-shrink: 0 to prevent shrinking | |
| html_output += '<div style="flex:1; min-width: 0;">' # min-width: 0 to allow flex item to shrink | |
| icon = "📖" if it["media"] == "book" else "🎬" | |
| html_output += f'<h3 style="margin:0 0 8px; font-size: 1.2em;">{icon} {it["title"]}</h3>' | |
| # Display primary creators/overview based on media type | |
| if it["media"] == "book": | |
| authors = ", ".join(it.get("authors", [])) | |
| html_output += f'<p style="margin:4px 0;"><strong>Author(s):</strong> {authors if authors else "N/A"}</p>' | |
| else: | |
| # Hide Director if N/A or empty | |
| director = it.get("director") | |
| if director and director != "N/A": | |
| html_output += f'<p style="margin:4px 0;"><strong>Director:</strong> {director}</p>' | |
| overview = it.get("overview", "No overview available.").strip() | |
| # Truncate long overviews for display | |
| if len(overview) > 200: | |
| overview = overview[:200] + "..." | |
| html_output += f'<p style="margin:4px 0;"><strong>Overview:</strong> {overview}</p>' | |
| html_output += f'<p style="margin:4px 0;"><strong>Genres:</strong> {", ".join(it.get("genres", []))}</p>' | |
| html_output += f'<p style="margin:4px 0;"><strong>Mood:</strong> {", ".join(it.get("mood", []))}</p>' | |
| aud = it.get("target_audience", "unknown").replace("_", " ") | |
| html_output += f'<p style="margin:4px 0;"><strong>Audience:</strong> {aud}</p>' | |
| # Display publication/release year and rating | |
| if it["media"] == "book": | |
| y = it.get("first_publish_year") | |
| html_output += f'<p style="margin:4px 0;"><strong>Published:</strong> {y}</p>' if y else "" | |
| else: | |
| yr = it.get("release_date", "").split("-")[0] | |
| html_output += f'<p style="margin:4px 0;"><strong>Year:</strong> {yr}</p>' if yr else "" | |
| r = it.get("vote_average") | |
| html_output += f'<p style="margin:4px 0;"><strong>Rating:</strong> {r:.1f}/10</p>' if r is not None else "" | |
| # Display extracted era and decade if available | |
| era_display = it.get("era", "N/A").replace("_", " ") | |
| decade_display = it.get("decade", "N/A") | |
| if era_display != "N/A" or decade_display != "N/A": | |
| time_parts = [] | |
| if era_display != "N/A": time_parts.append(era_display) | |
| if decade_display != "N/A": time_parts.append(decade_display) | |
| html_output += f'<p style="margin:4px 0;"><strong>Time Period:</strong> {", ".join(time_parts)}</p>' | |
| # Explanation from the retrieval model | |
| # --- MODIFICATION 2: Improve Explanation text color --- | |
| html_output += f'<p class="explanation" style="margin-top:10px; font-style: italic;"><strong>Explanation:</strong> {it["explanation"]}</p>' | |
| # --- END MODIFICATION 2 --- | |
| # External links | |
| if it["media"] == "book" and it.get("source_key"): | |
| html_output += f'<p style="margin-top:8px;"><a href="https://openlibrary.org{it["source_key"]}" target="_blank" style="text-decoration:none; color:#007bff;">🔗 View Book on Open Library</a></p>' | |
| if it["media"] == "movie" and it.get("tmdb_id"): | |
| html_output += f'<p style="margin-top:8px;"><a href="https://www.themoviedb.org/movie/{it["tmdb_id"]}" target="_blank" style="text-decoration:none; color:#007bff;">🔗 View Movie on TMDb</a></p>' | |
| html_output += "</div></div>" | |
| return html_output | |
| # --- Gradio Interface Definition --- | |
| with gr.Blocks(css=""" | |
| .gradio-container { | |
| padding: 1.5em; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| /* --- MODIFICATION 1: Change H2 (Main Title) color and style and the color of explanation --- */ | |
| @media (prefers-color-scheme: dark) { | |
| h2 { | |
| color: #FFFFFF; | |
| } | |
| p.explanation { | |
| color: #FFFFFF; | |
| } | |
| } | |
| @media (prefers-color-scheme: light) { | |
| h2 { | |
| color: #000000; | |
| } | |
| p.explanation { | |
| color: #000000; | |
| } | |
| } | |
| h2 { | |
| text-align: center; | |
| margin-bottom: 1.5em; | |
| font-size: 2.2em; | |
| font-weight: bold; | |
| } | |
| /* --- END MODIFICATION 1 --- */ | |
| h4 { | |
| margin-top: 1em; | |
| margin-bottom: 0.5em; | |
| color: #0056b3; /* A slightly darker blue for contrast */ | |
| font-size: 1.1em; | |
| } | |
| ul { | |
| list-style-type: none; | |
| padding: 0; | |
| margin: 0.5em 0; | |
| } | |
| ul li { | |
| margin-bottom: 0.2em; | |
| font-size: 0.95em; | |
| } | |
| .gr-button { | |
| background-color: #007bff; | |
| color: white; | |
| border-radius: 5px; | |
| } | |
| .gr-button:hover { | |
| background-color: #0056b3; | |
| } | |
| .gr-textbox, .gr-radio, .gr-slider { | |
| margin-bottom: 1em; | |
| } | |
| .gr-html { | |
| border: 1px solid #eee; | |
| padding: 1em; | |
| border-radius: 8px; | |
| background-color: #f9f9f9; | |
| } | |
| """) as demo: | |
| gr.Markdown("## 📖🎬 Intelligent Book & Movie Recommender") | |
| with gr.Row(): | |
| inp = gr.Textbox(lines=2, label="Your Query", | |
| placeholder="e.g., a heartwarming drama movie for young adults from the 90s by Quentin Tarantino") | |
| with gr.Row(): | |
| # User can still explicitly choose, but query parser can override | |
| choice_radio = gr.Radio(["Books", "Movies"], value="Books", label="Recommend Type (Query can override)") | |
| topk_slider = gr.Slider(1, 10, value=5, step=1, label="Number of Recommendations (Top-K)") | |
| mood_checkbox = gr.Checkbox(True, label="Mood-Aware Retrieval (Uses SBERT)") | |
| run_button = gr.Button("🔍 Get Recommendations") | |
| output_html = gr.HTML() | |
| # Bind the smart_recommend function to the button click event | |
| run_button.click( | |
| fn=smart_recommend, | |
| inputs=[inp, choice_radio, topk_slider, mood_checkbox], | |
| outputs=output_html | |
| ) | |
| # Optional: Add example queries | |
| gr.Examples( | |
| examples=[ | |
| "Recommend a thrilling sci-fi book by Isaac Asimov.", | |
| "A dark mystery by Agatha Christie.", | |
| "Show me action films for kids under 10.", | |
| "I need a romantic comedy released in the 2000s.", | |
| "Any classic historical fiction?", | |
| "looking for something uplifting for ages 18+", | |
| "A book about adventure for children.", | |
| "A suspenseful thriller for adults.", | |
| "A contemporary romance novel.", | |
| "I want a thriller by Stephen King.", | |
| "I'm feeling sad, recommend a melancholic movie.", | |
| "Give me an exciting thriller movie.", | |
| "I'm in the mood for something lighthearted.", | |
| "Looking for a really dark and grim book.", | |
| "Need something joyful to watch.", | |
| "I need an uplifting and inspiring film.", | |
| "Show me a truly gloomy and depressing story.", | |
| "Find me a film that's both chaotic and funny.", | |
| "I want something thought-provoking and deep.", | |
| "Looking for a movie that's really tense and nerve-wracking.", | |
| "Something wistful and nostalgic.", | |
| "I'm feeling angry, show me something intense and violent.", | |
| "Recommend a bizarre and absurd book.", | |
| "A beautiful and poignant love story.", | |
| "I need a really witty comedy.", | |
| "Something raw and gritty.", | |
| "A grand, sweeping epic.", | |
| "Something that brings tears to my eyes.", | |
| "Find me a slow-paced, meditative film.", | |
| "A mind-bending psychological thriller." | |
| ], | |
| inputs=inp | |
| ) | |
| demo.launch(share=True) |