EmotionPlaylist / app.py
ccolas's picture
Update app.py
9666561
raw
history blame
13.4 kB
import streamlit as st
import streamlit.components.v1 as components
from PIL import Image
import spotipy.util as util
import pickle
import spotipy
from utils import *
st.set_page_config(
page_title="EmotionPlaylist",
page_icon="🎧",
)
debug = False
dir_path = os.path.dirname(os.path.realpath(__file__))
st.title('Customize Emotional Playlists')
def centered_button(func, text, n_columns=7, args=None):
columns = st.columns(np.ones(n_columns))
with columns[n_columns//2]:
return func(text)
# get credentials
def setup_credentials():
if 'client_id' in os.environ.keys() and 'client_secret' in os.environ.keys():
client_info = dict(client_id=os.environ['client_id'],
client_secret=os.environ['client_secret'])
else:
with open(dir_path + "/ids.pk", 'rb') as f:
client_info = pickle.load(f)
os.environ['SPOTIPY_CLIENT_ID'] = client_info['client_id']
os.environ['SPOTIPY_CLIENT_SECRET'] = client_info['client_secret']
os.environ['SPOTIPY_REDIRECT_URI'] = 'https://huggingface.co/spaces/ccolas/EmotionPlaylist/'
relevant_audio_features = ["danceability", "energy", "loudness", "mode", "valence", "tempo"]
def get_client():
scope = "playlist-modify-public"
token = util.prompt_for_user_token(scope=scope)
sp = spotipy.Spotify(auth=token)
user_id = sp.me()['id']
return sp, user_id
def extract_uris_from_links(links, url_type):
assert url_type in ['playlist', 'artist', 'user']
urls = links.split('\n')
uris = []
for url in urls:
if 'playlist' in url:
uri = url.split(f'{url_type}/')[-1].split('?')[0]
else:
uri = url.split('?')[0]
uris.append(uri)
return uris
def wall_of_checkboxes(labels, max_width=10):
n_labels = len(labels)
n_rows = int(np.ceil(n_labels/max_width))
checkboxes = []
for i in range(n_rows):
columns = st.columns(np.ones(max_width))
row_length = n_labels % max_width if i == n_rows - 1 else max_width
for j in range(row_length):
with columns[j]:
checkboxes.append(st.empty())
return checkboxes
def aggregate_genres(genres, legit_genres, verbose=False):
genres_output = dict()
legit_genres_formatted = [lg.replace('-', '').replace(' ', '') for lg in legit_genres]
for glabel in genres.keys():
if verbose: print('\n', glabel)
glabel_formatted = glabel.replace(' ', '').replace('-', '')
best_match = None
best_match_score = 0
for legit_glabel, legit_glabel_formatted in zip(legit_genres, legit_genres_formatted):
if 'jazz' in glabel_formatted:
best_match = 'jazz'
if verbose: print('\t', 'pop')
break
if 'ukpop' in glabel_formatted:
best_match = 'pop'
if verbose: print('\t', 'pop')
break
if legit_glabel_formatted == glabel_formatted:
if verbose: print('\t', legit_glabel_formatted)
best_match = legit_glabel
break
elif glabel_formatted in legit_glabel_formatted:
if verbose: print('\t', legit_glabel_formatted)
if len(glabel_formatted) > best_match_score:
best_match = legit_glabel
best_match_score = len(glabel_formatted)
elif legit_glabel_formatted in glabel_formatted:
if verbose: print('\t', legit_glabel_formatted)
if len(legit_glabel_formatted) > best_match_score:
best_match = legit_glabel
best_match_score = len(legit_glabel_formatted)
if best_match is not None:
if verbose: print('\t', '-->', best_match)
if best_match in genres_output.keys():
genres_output[best_match] += genres[glabel]
else:
genres_output[best_match] = genres[glabel]
return genres_output
def setup_streamlite():
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 a source of candidate songs, select a list of genres and choose the mood for the playlist.")
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:
login = centered_button(st.button, 'Log in', n_columns=7)
if login or debug:
sp, user_id = get_client()
legit_genres = sp.recommendation_genre_seeds()['genres']
st.session_state['login'] = (sp, user_id, legit_genres)
if 'login' in st.session_state or debug:
if not debug: sp, user_id, legit_genres = st.session_state['login']
st.success('You are logged in.')
st.subheader("Step 2: Select candidate songs")
st.markdown("This can be done in three ways: \n"
"1. Get songs from a list of artists\n"
"2. Get songs from a list of users (and their playlists)\n"
"3. Get songs from a list of playlists.\n"
"For this you'll need to collect the urls of artists, users and/or playlists by clicking on 'Share' and copying the urls."
"You need to provide at least one source of music.")
label_artist = "Add a list of artist urls, one per line (optional)"
artists_links = st.text_area(label_artist, value="")
users_playlists = "Add a list of users 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?si=529184bbe93c4f73")
button = centered_button(st.button, 'Extract music', n_columns=5)
if button or debug:
if playlist_links != "":
playlist_uris = extract_uris_from_links(playlist_links, url_type='playlist')
else:
raise ValueError('Please enter a list of playlists')
# Scanning playlists
st.spinner(text="Scanning music sources..")
data_tracks = get_all_tracks_from_playlists(sp, playlist_uris, verbose=True)
st.success(f'{len(data_tracks.keys())} tracks found!')
# Extract audio features
st.spinner(text="Extracting 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]
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]))
genres = dict()
for index, uri in enumerate(all_tracks_uris):
track = data_tracks[uri]
track_genres = track['track']['genres']
for g in track_genres:
if g not in genres.keys():
genres[g] = [index]
else:
genres[g].append(index)
genres = aggregate_genres(genres, legit_genres)
genres_labels = sorted(genres.keys())
st.success(f'Audio features extracted!')
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)
if 'music_extracted' in st.session_state.keys():
all_tracks_uris = st.session_state['music_extracted']['all_tracks_uris']
all_tracks_audio_features = st.session_state['music_extracted']['all_tracks_audio_features']
genres = st.session_state['music_extracted']['genres']
genres_labels = st.session_state['music_extracted']['genres_labels']
st.subheader("Step 3: Customize it!")
st.markdown("##### Which genres?")
st.markdown("Check boxes to select genres, see how many tracks were selected below. 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():
st.session_state['checkboxes'] = [True] * len(genres_labels)
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])
# filter songs by genres
selected_labels = [genres_labels[i] for i in range(len(genres_labels)) if st.session_state['checkboxes'][i]]
genre_selected_indexes = []
for label in selected_labels:
genre_selected_indexes += genres[label]
genre_selected_indexes = np.array(sorted(set(genre_selected_indexes)))
if len(genre_selected_indexes) < 10:
st.warning('Please select more genres or add more music sources.')
else:
st.success(f'{len(genre_selected_indexes)} candidate tracks selected.')
st.markdown("##### What's the mood?")
valence = st.slider('Valence (0 negative, 100 positive)', min_value=0, max_value=100, value=100, step=1) / 100
energy = st.slider('Energy (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
danceability = st.slider('Danceability (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
target_mood = np.array([valence, energy, danceability]).reshape(1, 3)
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]
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//5), max_value=n_candidates//5)
selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:playlist_length]]
selected_tracks_uris = all_tracks_uris[selected_tracks_indexes]
np.random.shuffle(selected_tracks_uris)
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:
description = f'Emotion Playlist for Valence: {valence}, Energy: {energy}, Danceability: {danceability}). ' \
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__':
setup_streamlite()