Spaces:
Runtime error
Runtime error
File size: 21,797 Bytes
bc64e5a cb32cb9 80d5795 3dd7abd 80d5795 1767f84 ca32a6a 36321f6 3dd7abd cb32cb9 dde73a1 cb32cb9 dde73a1 cb32cb9 80d5795 cb32cb9 3dd7abd bc64e5a 3dd7abd bc64e5a cb32cb9 bc64e5a fad5ae9 3dd7abd bc64e5a fad5ae9 bc64e5a cb32cb9 300f883 80d5795 300f883 80d5795 bc64e5a 80d5795 300f883 80d5795 cb32cb9 80d5795 300f883 80d5795 dbea0da 80d5795 dbea0da 1767f84 dbea0da 80d5795 dde73a1 80d5795 300f883 80d5795 dbea0da 80d5795 dbea0da d7dfcdc 80d5795 dbea0da 80d5795 0b9ab43 80d5795 cb32cb9 80d5795 300f883 80d5795 300f883 dbea0da 300f883 dbea0da cb32cb9 dbea0da 80d5795 bc64e5a dde73a1 bc64e5a dbea0da bc64e5a dbea0da bc64e5a 80d5795 bc64e5a 80d5795 bc64e5a 80d5795 dbea0da 80d5795 dbea0da 80d5795 cb32cb9 bc64e5a 80d5795 cb32cb9 bc64e5a 0b9ab43 bc64e5a dbea0da bc64e5a dbea0da bc64e5a dbea0da bc64e5a dbea0da bc64e5a 563e52e bc64e5a dbea0da bc64e5a dbea0da bc64e5a dbea0da cb32cb9 80d5795 3dd7abd 80d5795 cb32cb9 80d5795 dde73a1 bc64e5a 80d5795 dbea0da 3dd7abd 80d5795 dbea0da 80d5795 dbea0da 80d5795 dbea0da 80d5795 cb32cb9 80d5795 bc64e5a 80d5795 cb32cb9 80d5795 bc64e5a 80d5795 dbea0da bc64e5a 80d5795 dbea0da 0b9ab43 bc64e5a 300f883 80d5795 cb32cb9 80d5795 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
import requests
import streamlit
from PIL import Image
from utils import *
from app_utils import *
import time
from spotipy.oauth2 import SpotifyClientCredentials
debug = False
dir_path = os.path.dirname(os.path.realpath(__file__))
st.set_page_config(
page_title="EmotionalPlaylist",
page_icon="🎧",
)
st.title('Emotional Playlists')
def log_to_spotify():
st.subheader("Step 1: Connect to your Spotify app")
st.markdown("Log into your Spotify account to let the app create the custom playlist.")
if 'login' not in st.session_state or debug:
if debug:
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
user_id = None
auth_manager = None
else:
sp, user_id, auth_manager = new_get_client(session=st.session_state)
if sp != None:
legit_genres = sp.recommendation_genre_seeds()['genres']
st.session_state['login'] = (sp, user_id, legit_genres, auth_manager)
st.success('You are logged in.')
else:
legit_genres = None
else:
sp, user_id, legit_genres, auth_manager = st.session_state['login']
st.success('You are logged in.')
return sp, user_id, legit_genres, auth_manager
@st.cache(suppress_st_warning=True)
def get_user_playlists(users_links):
global sp
# Scanning users
n_playlists = 0
all_uris, all_names = [], []
if users_links != "":
try:
print(users_links)
user_ids = extract_uris_from_links(users_links, url_type='user')
print(user_ids)
all_uris, all_names = get_all_playlists_uris_from_users(sp, user_ids)
n_playlists = len(all_uris)
except:
st.warning('Please enter a valid list of user names (one url per line)')
return all_uris, all_names, n_playlists
def get_filtered_user_playlists(user_links):
global sp
st.spinner(text="Scanning users..")
all_uris, all_names, n_playlists = get_user_playlists(user_links)
if n_playlists <= 1:
return all_uris
else:
with st.expander("##### Select user playlists (default all)"):
# let the user uncheck playlists
st.markdown("Check boxes to select playlists from the selected users."
"Note: to check all, first uncheck all (bug).")
columns = st.columns(np.ones(5))
with columns[1]:
check_all_playlists = st.button('Check all')
with columns[3]:
uncheck_all_playlists = st.button('Uncheck all')
if 'checkboxes' not in st.session_state.keys():
st.session_state['checkboxes_playlists'] = [True] * n_playlists
empty_checkboxes = wall_of_checkboxes(all_names, max_width=5)
if check_all_playlists:
st.session_state['checkboxes_playlists'] = [True] * n_playlists
if uncheck_all_playlists:
st.session_state['checkboxes_playlists'] = [False] * n_playlists
for i_emc, emc in enumerate(empty_checkboxes):
st.session_state['checkboxes_playlists'][i_emc] = emc.checkbox(all_names[i_emc], value=st.session_state['checkboxes_playlists'][i_emc])
filter_playlist = centered_button(st.button, 'Update user playlists', n_columns=5)
if filter_playlist:
return list(np.array(all_uris)[np.where(st.session_state['checkboxes_playlists'])])
else:
return []
@st.cache(suppress_st_warning=True)
def get_non_user_playlists(playlist_links):
# Scanning playlists
new_playlist_uris = []
if playlist_links != "":
st.spinner(text="Scanning playlists..")
try:
new_playlist_uris = extract_uris_from_links(playlist_links, url_type='playlist')
except:
st.warning('Please enter a valid list of playlists (one url per line)')
return new_playlist_uris
@st.cache
def extract_tracks(playlist_uris):
global sp
# extracting tracks
data_tracks = get_all_tracks_from_playlists(sp, playlist_uris, verbose=True)
return data_tracks
@st.cache
def extract_audio_features(data_tracks, legit_genres):
# Extract audio features
all_tracks_uris = np.array(list(data_tracks.keys()))
all_audio_features = [data_tracks[uri]['track']['audio_features'] for uri in all_tracks_uris]
valid_indexes = np.array([i for i in range(len(all_tracks_uris)) if all_audio_features[i] is not None])
all_tracks_uris = all_tracks_uris[valid_indexes]
all_audio_features = np.array(all_audio_features)[valid_indexes]
all_tracks_audio_features = dict(zip(relevant_audio_features, [[audio_f[k] for audio_f in all_audio_features] for k in relevant_audio_features]))
all_tracks_genres = []
indexes_by_genre = dict()
for index, uri in enumerate(all_tracks_uris):
track = data_tracks[uri]
track_genres = track['track']['genres']
all_tracks_genres.append([])
for glabel in track_genres:
legit_genre = find_legit_genre(glabel, legit_genres)
if legit_genre in indexes_by_genre.keys():
indexes_by_genre[legit_genre].append(index)
else:
indexes_by_genre[legit_genre] = [index]
all_tracks_genres[-1].append(legit_genre)
all_tracks_genres[-1] = sorted(set(all_tracks_genres[-1]))
genres_labels = sorted(indexes_by_genre.keys())
all_tracks_genres = np.array(all_tracks_genres)
return all_tracks_uris, all_tracks_audio_features, all_tracks_genres, indexes_by_genre, genres_labels
# st.session_state['music_extracted'] = dict(all_tracks_uris=all_tracks_uris,
# all_tracks_audio_features=all_tracks_audio_features,
# genres=genres,
# genres_labels=genres_labels)
def select_songs(legit_genres):
global sp
st.subheader("Step 2: Select candidate songs")
st.markdown("This can be done in two ways: \n"
"1. Get songs from a list of users (and their playlists)\n"
"2. Get songs from a list of playlists.\n"
"For this you'll need to collect user and/or playlist urls by clicking on \"Share\" and \"Copy link\" in the Spotify app.")
users_playlists = "Add a list of user urls, one per line (optional)"
users_links = st.text_area(users_playlists, value="")
label_playlists = "Add a list of playlists urls, one per line (optional)"
playlist_links = st.text_area(label_playlists, value="https://open.spotify.com/playlist/1H7a4q8JZArMQiidRy6qon\nhttps://open.spotify.com/playlist/6wbaZqht4w6CMv3od5taax?si=5c6ebe13fdd049b6")
extract_button = centered_button(st.button, 'Extract music', n_columns=5)
all_tracks_uris, all_tracks_audio_features, all_tracks_genres, indexes_by_genre, genres_labels = [None] * 5
updated_sources = False
if extract_button or debug or 'extract_button' in st.session_state.keys():
if extract_button:
updated_sources = True
st.session_state['extract_button'] = True
# check the user input music sourc
if playlist_links == "" and users_links == "":
st.warning('Please enter at least one source of music.')
else:
st.spinner(text="Scanning music sources..")
playlist_uris = []
init_time = time.time()
init_time_tot = init_time
user_playlists = get_filtered_user_playlists(users_links)
playlist_uris += user_playlists
print(f'1. user playlist: {time.time() - init_time:.2f}')
init_time = time.time()
new_playlist_uris = get_non_user_playlists(playlist_links)
playlist_uris += new_playlist_uris
n_users = len(users_links.split('\n'))
st.success(f'{len(playlist_uris)} new playlists added from {n_users} users.')
print(f'2. non user playlist: {time.time() - init_time:.2f}')
init_time = time.time()
if str(playlist_uris) in st.session_state.keys():
data_tracks = st.session_state[str(playlist_uris)]
else:
data_tracks = extract_tracks(playlist_uris)
st.session_state[str(playlist_uris)] = data_tracks
print(f'3. track extraction: {time.time() - init_time:.2f}')
init_time = time.time()
if len(data_tracks.keys()) < 10:
st.warning('Please select more music sources.')
else:
all_tracks_uris, all_tracks_audio_features, all_tracks_genres, indexes_by_genre, genres_labels = extract_audio_features(data_tracks, legit_genres)
print(f'4. audio feature extraction: {time.time() - init_time:.2f}')
print(f'\t total extraction: {time.time() - init_time_tot:.2f}')
st.success(f'{len(data_tracks.keys())} tracks found!')
return all_tracks_uris, all_tracks_audio_features, all_tracks_genres, indexes_by_genre, genres_labels, updated_sources
def customize_widgets(genres_labels, updated_sources):
st.subheader("Step 3: Customize it!")
st.markdown('##### Which genres?')
expanded = True if 'expanded_genres' in st.session_state else False
with st.expander("Unroll to select (default all)", expanded=expanded):
st.session_state['expanded_genres'] = True
st.markdown("Check boxes to select genres. Note: to check all, first uncheck all (bug).")
columns = st.columns(np.ones(5))
with columns[1]:
check_all = st.button('Check all')
with columns[3]:
uncheck_all = st.button('Uncheck all')
if 'checkboxes' not in st.session_state.keys() or updated_sources:
st.session_state['checkboxes'] = [True] * len(genres_labels)
updated_sources = False
empty_checkboxes = wall_of_checkboxes(genres_labels, max_width=5)
if check_all:
st.session_state['checkboxes'] = [True] * len(genres_labels)
if uncheck_all:
st.session_state['checkboxes'] = [False] * len(genres_labels)
for i_emc, emc in enumerate(empty_checkboxes):
st.session_state['checkboxes'][i_emc] = emc.checkbox(genres_labels[i_emc], value=st.session_state['checkboxes'][i_emc])
st.markdown("##### What's the mood?")
valence = st.slider('Valence (0 negative, 100 positive)', min_value=0, max_value=100, value=60, step=1) / 100
energy = st.slider('Energy (0 low, 100 high)', min_value=0, max_value=100, value=60, step=1) / 100
danceability = st.slider('Danceability (0 low, 100 high)', min_value=0, max_value=100, value=60, step=1) / 100
target_mood = np.array([valence, energy, danceability]).reshape(1, 3)
streamlit.markdown('##### Shall we explore?')
streamlit.write("Set the strength of music exploration:\n"
"* 0%: all songs are selected from the music sources\n"
"* 100%: all songs are new.")
exploration = st.slider('Exploration (0%, 100%)', min_value=0, max_value=100, value=50, step=1) / 100
return target_mood, exploration
@st.cache
def filter_songs_by_genre(checkboxes, genres_labels, indexes_by_genre):
# filter songs by genres
selected_labels = [genres_labels[i] for i in range(len(genres_labels)) if checkboxes[i]]
genre_selected_indexes = []
for label in selected_labels:
genre_selected_indexes += indexes_by_genre[label]
genre_selected_indexes = np.array(sorted(set(genre_selected_indexes)))
return genre_selected_indexes
@st.cache
def find_best_songs_for_mood(all_tracks_audio_features, genre_selected_indexes, target_mood):
candidate_moods = np.array([np.array(all_tracks_audio_features[feature])[genre_selected_indexes] for feature in ['valence', 'energy', 'danceability']]).T
distances = np.sqrt(((candidate_moods - target_mood) ** 2).sum(axis=1))
min_dist_indexes = np.argsort(distances)
n_candidates = distances.shape[0]
return min_dist_indexes, n_candidates
@st.cache
def run_exploration(selected_tracks_uris, selected_tracks_genres, playlist_length, exploration, all_tracks_uris, target_mood):
# sample exploration songs
if exploration > 0:
n_known = int(playlist_length * (1 - exploration))
n_new = playlist_length - n_known
print(f'Number of new songs: {n_new}, known songs: {n_known}')
known_songs = selected_tracks_uris[:n_known]
seed_songs = selected_tracks_uris[-n_new:]
seed_genres = selected_tracks_genres[-n_new:]
dict_args = dict() # enforce bounds on recommendations' moods
for i_m, m in enumerate(['valence', 'energy', 'danceability']):
dict_args[f'min_{m}'] = max(0, target_mood[i_m] - 0.1)
dict_args[f'max_{m}'] = min(1, target_mood[i_m] + 0.1)
dict_args_loose = dict() # enforce bounds on recommendations' moods
for i_m, m in enumerate(['valence', 'energy', 'danceability']):
dict_args_loose[f'min_{m}'] = max(0, target_mood[i_m] - 0.2)
dict_args_loose[f'max_{m}'] = min(1, target_mood[i_m] + 0.2)
dict_args_looser = dict() # enforce bounds on recommendations' moods
for i_m, m in enumerate(['valence', 'energy', 'danceability']):
dict_args_loose[f'min_{m}'] = max(0, target_mood[i_m] - 0.3)
dict_args_loose[f'max_{m}'] = min(1, target_mood[i_m] + 0.3)
new_songs = []
counter_seed = 0
while len(new_songs) < n_new:
try:
print(seed_songs[counter_seed])
print(dict_args)
reco = sp.recommendations(seed_tracks=[seed_songs[counter_seed]], seed_genres=seed_genres[counter_seed],
market="from_token", country='from_token', **dict_args)['tracks']
if len(reco) == 0:
print('Using loose bounds')
reco = sp.recommendations(seed_tracks=[seed_songs[counter_seed]], seed_genres=seed_genres[counter_seed],
market="from_token", country='from_token', **dict_args_loose)['tracks']
if len(reco) == 0:
print('Using looser bounds')
reco = sp.recommendations(seed_tracks=[seed_songs[counter_seed]], seed_genres=seed_genres[counter_seed],
market="from_token", country='from_token', **dict_args_looser)['tracks']
if len(reco) == 0:
print('Removing bounds')
reco = sp.recommendations(seed_tracks=[seed_songs[counter_seed]], market="from_token")['tracks']
assert len(reco) > 0
for r in reco:
if r['uri'] not in all_tracks_uris and r['uri'] not in new_songs:
new_songs.append(r['uri'])
break
except:
pass
print(counter_seed, len(new_songs))
counter_seed = (counter_seed + 1) % len(seed_songs)
assert len(new_songs) == n_new
assert len(known_songs) == n_known
selected_tracks_uris = np.array(list(known_songs) + new_songs)
np.random.shuffle(selected_tracks_uris)
return selected_tracks_uris
@st.cache
def sample_playlist(n_candidates, playlist_length, genre_selected_indexes, min_dist_indexes, all_tracks_uris, all_tracks_genres):
# give more freedom to randomize the playlist
if n_candidates > 5 * playlist_length:
selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:int(playlist_length * 2)]]
else:
selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:playlist_length]]
shuffled_indexes = np.arange(len(selected_tracks_indexes))
np.random.shuffle(shuffled_indexes)
selected_tracks_uris = all_tracks_uris[selected_tracks_indexes][shuffled_indexes]
selected_tracks_genres = all_tracks_genres[selected_tracks_indexes][shuffled_indexes]
selected_tracks_uris = selected_tracks_uris[:playlist_length]
selected_tracks_genres = selected_tracks_genres[:playlist_length]
return selected_tracks_uris, selected_tracks_genres
def run_app():
global sp
setup_credentials()
image = Image.open(dir_path + '/image.png')
st.image(image)
st.markdown("This app let's you quickly build playlists in a customized way: ")
st.markdown("* **It's easy**: you won't have to add songs one by one,\n"
"* **You're in control**: you provide the source of songs, select genres and pick the mood,\n"
"* **You're free to explore**: set the exploration strength from no new songs to all new songs.")
sp, user_id, legit_genres, auth_manager = log_to_spotify()
if 'login' in st.session_state or debug:
all_tracks_uris, all_tracks_audio_features, all_tracks_genres, indexes_by_genre, genres_labels, updated_sources = select_songs(legit_genres)
if all_tracks_uris is not None:
target_mood, exploration = customize_widgets(genres_labels, updated_sources)
custom_button = centered_button(st.button, 'Run customization', n_columns=5)
if custom_button or 'run_custom' in st.session_state.keys() or debug:
st.session_state['run_custom'] = True
checkboxes = st.session_state['checkboxes'].copy()
init_time = time.time()
genre_selected_indexes = filter_songs_by_genre(checkboxes, genres_labels, indexes_by_genre)
if len(genre_selected_indexes) < 10:
genre_selected_indexes = None
st.warning('Please select more genres or add more music sources.')
else:
st.success(f'{len(genre_selected_indexes)} candidate tracks selected.')
print(f'6. filter by genre: {time.time() - init_time:.2f}')
init_time = time.time()
if genre_selected_indexes is not None:
min_dist_indexes, n_candidates = find_best_songs_for_mood(all_tracks_audio_features, genre_selected_indexes, target_mood)
print(f'7. filter by mood: {time.time() - init_time:.2f}')
init_time = time.time()
if n_candidates < 25:
st.warning('Please add more music sources or select more genres.')
else:
playlist_length = st.number_input(f'Pick a playlist length, given {n_candidates} candidates.', min_value=5,
value=min(10, n_candidates//3), max_value=n_candidates//3)
selected_tracks_uris, selected_tracks_genres = sample_playlist(n_candidates, playlist_length, genre_selected_indexes,
min_dist_indexes, all_tracks_uris, all_tracks_genres)
print(f'8. Sample songs: {time.time() - init_time:.2f}')
init_time = time.time()
playlist_name = st.text_input('Playlist name', value='Mood Playlist')
if playlist_name == '':
st.warning('Please enter a playlist name.')
else:
generation_button = centered_button(st.button, 'Generate playlist', n_columns=5)
if generation_button:
selected_tracks_uris = run_exploration(selected_tracks_uris, selected_tracks_genres, playlist_length, exploration, all_tracks_uris,
target_mood.flatten())
print(f'9. run exploration: {time.time() - init_time:.2f}')
init_time = time.time()
target_mood = np.array(target_mood).flatten() * 100
description = f'Emotion Playlist for Valence: {int(target_mood[0])}, ' \
f'Energy: {int(target_mood[1])}, ' \
f'Danceability: {int(target_mood[2])}). ' \
f'Playlist generated by the EmotionPlaylist app: https://huggingface.co/spaces/ccolas/EmotionPlaylist.'
playlist_info = sp.user_playlist_create(user_id, playlist_name, public=True, collaborative=False, description=description)
playlist_uri = playlist_info['uri'].split(':')[-1]
sp.playlist_add_items(playlist_uri, selected_tracks_uris)
st.write(
f"""
<html>
<body>
<center>
<iframe style = "border-radius:12px" src="https://open.spotify.com/embed/playlist/{playlist_uri}" allowtransparency="true"
allow="encrypted-media" width="80%" height="580" frameborder="0"></iframe></center></body></html>
""", unsafe_allow_html=True)
st.success(f'The playlist has been generated, find it [here](https://open.spotify.com/playlist/{playlist_uri}).')
stop = 1
if __name__ == '__main__':
run_app() |