Recommender / app.py
badminton001's picture
Update app.py
a6c4db9 verified
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)