Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
from transformers import pipeline
|
4 |
+
|
5 |
+
# Enter your TMDb API key here
|
6 |
+
TMDB_API_KEY = '2cc77b08a475b510f54a0dcca2c5c0c7'
|
7 |
+
IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w200" # TMDb base URL for poster images (width 200)
|
8 |
+
|
9 |
+
@st.cache_resource
|
10 |
+
def load_emotion_analyzer():
|
11 |
+
return pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base")
|
12 |
+
|
13 |
+
emotion_analyzer = load_emotion_analyzer()
|
14 |
+
|
15 |
+
def analyze_emotion(description):
|
16 |
+
"""Analyze emotion of a movie's description."""
|
17 |
+
emotions = emotion_analyzer(description)
|
18 |
+
return emotions[0]['label'] # Return the top emotion
|
19 |
+
|
20 |
+
def get_movie_genres():
|
21 |
+
"""Fetches movie genres from TMDb API and returns a dictionary of genres."""
|
22 |
+
url = f'https://api.themoviedb.org/3/genre/movie/list?api_key={TMDB_API_KEY}&language=en-US'
|
23 |
+
response = requests.get(url)
|
24 |
+
if response.status_code == 200:
|
25 |
+
genres = response.json().get('genres', [])
|
26 |
+
return {genre['name']: genre['id'] for genre in genres}
|
27 |
+
else:
|
28 |
+
return None # Return None if the request failed
|
29 |
+
|
30 |
+
def get_top_movies_by_genre(genre_id):
|
31 |
+
"""Fetches the top 10 highest-rated movies for a given genre ID from TMDb, sorted by title length."""
|
32 |
+
url = f'https://api.themoviedb.org/3/discover/movie?api_key={TMDB_API_KEY}&language=en-US&sort_by=vote_average.desc&vote_count.gte=1000&with_genres={genre_id}'
|
33 |
+
response = requests.get(url)
|
34 |
+
if response.status_code == 200:
|
35 |
+
movies = response.json().get('results', [])[:10] # Get the top 10 results
|
36 |
+
# Sort movies by title length, shortest to longest
|
37 |
+
return sorted([(movie['title'], movie['vote_average'], movie['id'], movie['poster_path']) for movie in movies], key=lambda x: len(x[0]))
|
38 |
+
else:
|
39 |
+
return None
|
40 |
+
|
41 |
+
def get_similar_movies(movie_id, genre_id):
|
42 |
+
"""Fetches similar movies for a given movie ID from TMDb and filters by the selected genre."""
|
43 |
+
url = f'https://api.themoviedb.org/3/movie/{movie_id}/similar?api_key={TMDB_API_KEY}&language=en-US'
|
44 |
+
response = requests.get(url)
|
45 |
+
if response.status_code == 200:
|
46 |
+
movies = response.json().get('results', [])[:15]
|
47 |
+
# Include the movie description (overview) in the returned values
|
48 |
+
return [
|
49 |
+
(movie['title'], movie['vote_average'], movie['poster_path'], movie['id'], movie['overview'])
|
50 |
+
for movie in movies if genre_id in movie['genre_ids'] and 'overview' in movie
|
51 |
+
]
|
52 |
+
else:
|
53 |
+
return None
|
54 |
+
|
55 |
+
def get_movie_details(movie_id):
|
56 |
+
"""Fetches detailed information for a given movie ID from TMDb, including description, release date, director, and cast."""
|
57 |
+
url = f'https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US&append_to_response=credits'
|
58 |
+
response = requests.get(url)
|
59 |
+
if response.status_code == 200:
|
60 |
+
movie = response.json()
|
61 |
+
directors = [member['name'] for member in movie['credits']['crew'] if member['job'] == 'Director']
|
62 |
+
cast = [member['name'] for member in movie['credits']['cast'][:5]] # Get top 5 cast members
|
63 |
+
return {
|
64 |
+
"title": movie['title'],
|
65 |
+
"rating": movie['vote_average'],
|
66 |
+
"description": movie['overview'],
|
67 |
+
"poster_path": movie['poster_path'],
|
68 |
+
"release_date": movie['release_date'],
|
69 |
+
"director": ", ".join(directors),
|
70 |
+
"cast": ", ".join(cast)
|
71 |
+
}
|
72 |
+
else:
|
73 |
+
return None
|
74 |
+
|
75 |
+
# Initialize session state to store selected movies and recommendations
|
76 |
+
if 'selected_movies' not in st.session_state:
|
77 |
+
st.session_state['selected_movies'] = set()
|
78 |
+
if 'recommendations' not in st.session_state:
|
79 |
+
st.session_state['recommendations'] = []
|
80 |
+
if 'last_genre' not in st.session_state:
|
81 |
+
st.session_state['last_genre'] = None
|
82 |
+
|
83 |
+
# Streamlit app title
|
84 |
+
st.title("Movie Recommender")
|
85 |
+
|
86 |
+
# CSS for alignment and button styling
|
87 |
+
st.markdown("""
|
88 |
+
<style>
|
89 |
+
.movie-container {
|
90 |
+
display: flex;
|
91 |
+
flex-direction: column;
|
92 |
+
align-items: center;
|
93 |
+
justify-content: flex-start;
|
94 |
+
}
|
95 |
+
.movie-caption {
|
96 |
+
min-height: 50px; /* Set a consistent height for caption sections */
|
97 |
+
display: flex;
|
98 |
+
align-items: center;
|
99 |
+
justify-content: center;
|
100 |
+
text-align: center;
|
101 |
+
}
|
102 |
+
.movie-caption button {
|
103 |
+
width: 100%;
|
104 |
+
}
|
105 |
+
</style>
|
106 |
+
""", unsafe_allow_html=True)
|
107 |
+
|
108 |
+
# Fetch genres from TMDb and display in a dropdown
|
109 |
+
genres = get_movie_genres()
|
110 |
+
if genres is None:
|
111 |
+
st.error("Failed to load movie genres. Please check your API key or connection.")
|
112 |
+
else:
|
113 |
+
genre_name = st.selectbox("Select a movie genre:", [""] + list(genres.keys())) # Include an empty option
|
114 |
+
|
115 |
+
# Check if genre has changed, and reset selections if so
|
116 |
+
if genre_name != st.session_state['last_genre']:
|
117 |
+
st.session_state['selected_movies'].clear()
|
118 |
+
st.session_state['recommendations'].clear()
|
119 |
+
st.session_state['last_genre'] = genre_name
|
120 |
+
|
121 |
+
# Only fetch and display movies if a genre is selected
|
122 |
+
if genre_name:
|
123 |
+
genre_id = genres[genre_name] # Get the genre ID based on the selected genre name
|
124 |
+
top_movies = get_top_movies_by_genre(genre_id) # Fetch top 10 movies in this genre, sorted by title length
|
125 |
+
|
126 |
+
# Check if movies were successfully loaded
|
127 |
+
if top_movies is None:
|
128 |
+
st.error("Failed to load movies. Please check your API key or connection.")
|
129 |
+
else:
|
130 |
+
# Display the top 10 movies as clickable titles in a 5x2 grid layout with aligned rows
|
131 |
+
st.write(f"Top 10 movies in the {genre_name} genre:")
|
132 |
+
columns = st.columns(5) # Create 5 columns for each row
|
133 |
+
|
134 |
+
# First row of 5 movies
|
135 |
+
for index, (title, _, movie_id, poster_path) in enumerate(top_movies[:5]):
|
136 |
+
col = columns[index]
|
137 |
+
with col:
|
138 |
+
st.markdown("<div class='movie-container'>", unsafe_allow_html=True)
|
139 |
+
|
140 |
+
# Display image
|
141 |
+
st.image(f"{IMAGE_BASE_URL}{poster_path}", use_container_width=True)
|
142 |
+
|
143 |
+
# Display clickable title as a button to toggle selection, with consistent height
|
144 |
+
if st.button(f"{title} {'✅' if movie_id in st.session_state['selected_movies'] else ''}", key=f"title_{movie_id}"):
|
145 |
+
if movie_id in st.session_state['selected_movies']:
|
146 |
+
st.session_state['selected_movies'].remove(movie_id)
|
147 |
+
else:
|
148 |
+
st.session_state['selected_movies'].add(movie_id)
|
149 |
+
|
150 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
151 |
+
|
152 |
+
# Second row of 5 movies
|
153 |
+
for index, (title, _, movie_id, poster_path) in enumerate(top_movies[5:]):
|
154 |
+
col = columns[index]
|
155 |
+
with col:
|
156 |
+
st.markdown("<div class='movie-container'>", unsafe_allow_html=True)
|
157 |
+
|
158 |
+
# Display image
|
159 |
+
st.image(f"{IMAGE_BASE_URL}{poster_path}", use_container_width=True)
|
160 |
+
|
161 |
+
# Display clickable title as a button to toggle selection, with consistent height
|
162 |
+
if st.button(f"{title} {'✅' if movie_id in st.session_state['selected_movies'] else ''}", key=f"title_2_{movie_id}"):
|
163 |
+
if movie_id in st.session_state['selected_movies']:
|
164 |
+
st.session_state['selected_movies'].remove(movie_id)
|
165 |
+
else:
|
166 |
+
st.session_state['selected_movies'].add(movie_id)
|
167 |
+
|
168 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
169 |
+
|
170 |
+
# Button to generate recommendations based on selected movies
|
171 |
+
if st.button("Show Recommendations"):
|
172 |
+
if st.session_state['selected_movies']:
|
173 |
+
# Step 1: Collect emotions of user-selected movies
|
174 |
+
st.write("Analyzing emotions of selected movies...")
|
175 |
+
selected_emotions = []
|
176 |
+
for movie_id in st.session_state['selected_movies']:
|
177 |
+
movie_details = get_movie_details(movie_id)
|
178 |
+
if movie_details:
|
179 |
+
emotion = analyze_emotion(movie_details["description"])
|
180 |
+
selected_emotions.append(emotion)
|
181 |
+
st.write(f"**{movie_details['title']}** - Emotion: {emotion}")
|
182 |
+
|
183 |
+
# Step 2: Collect similar movies for all selected movies
|
184 |
+
all_similar_movies = []
|
185 |
+
for movie_id in st.session_state['selected_movies']:
|
186 |
+
similar_movies = get_similar_movies(movie_id, genre_id)
|
187 |
+
if similar_movies:
|
188 |
+
all_similar_movies.extend(similar_movies)
|
189 |
+
|
190 |
+
# Step 3: Filter similar movies by matching emotions and remove duplicates
|
191 |
+
recommendations = []
|
192 |
+
seen_movie_ids = set(st.session_state['selected_movies']) # Start with user-selected movies to exclude them
|
193 |
+
|
194 |
+
with st.spinner("Filtering movies by matching emotions..."):
|
195 |
+
for title, rating, poster_path, movie_id, description in all_similar_movies:
|
196 |
+
if movie_id not in seen_movie_ids: # Exclude already-seen movies
|
197 |
+
movie_emotion = analyze_emotion(description)
|
198 |
+
if movie_emotion in selected_emotions:
|
199 |
+
seen_movie_ids.add(movie_id) # Mark this movie as seen to prevent duplicates
|
200 |
+
recommendations.append((title, rating, poster_path, movie_id, movie_emotion))
|
201 |
+
|
202 |
+
# Store unique recommendations in session state
|
203 |
+
st.session_state['recommendations'] = recommendations
|
204 |
+
|
205 |
+
# Step 4: Display final recommendations
|
206 |
+
if st.session_state['recommendations']:
|
207 |
+
st.write("Based on your selections, you might also enjoy these top 5 movies:")
|
208 |
+
for title, rating, poster_path, movie_id, emotion in sorted(st.session_state['recommendations'], key=lambda x: x[1], reverse=True)[:5]:
|
209 |
+
movie_details = get_movie_details(movie_id)
|
210 |
+
if movie_details:
|
211 |
+
col1, col2 = st.columns([1, 3])
|
212 |
+
with col1:
|
213 |
+
st.image(f"{IMAGE_BASE_URL}{movie_details['poster_path']}", use_container_width=True)
|
214 |
+
with col2:
|
215 |
+
st.markdown(f"**Title:** {movie_details['title']}")
|
216 |
+
st.markdown(f"**TMDb Rating:** {movie_details['rating']}")
|
217 |
+
st.markdown(f"**Emotion:** {emotion}")
|
218 |
+
st.markdown(f"**Description:** {movie_details['description']}")
|
219 |
+
|
220 |
+
# Additional Information button with overlay using expander
|
221 |
+
with st.expander("Additional Information"):
|
222 |
+
st.markdown(f"**Release Date:** {movie_details['release_date']}")
|
223 |
+
st.markdown(f"**Director:** {movie_details['director']}")
|
224 |
+
st.markdown(f"**Cast:** {movie_details['cast']}")
|
225 |
+
else:
|
226 |
+
st.warning("No movies matched your selected emotions.")
|
227 |
+
else:
|
228 |
+
st.warning("Please select at least one movie to generate recommendations.")
|