carterwward commited on
Commit
9b43d0b
1 Parent(s): 912bb65

original files

Browse files
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /venv
2
+ /venv/bin/*
3
+ .cache
4
+ *.ipynb
5
+ /src/config
6
+ /data/spotify_million_playlist_dataset_challenge
7
+ /data/songs_new.csv
8
+ __pycache__
9
+ *.code-workspace
app.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.playlist_builder import PlaylistBuilder
2
+ """
3
+ NOTES:
4
+ TODO NEXT:
5
+ - add seeding through api search.
6
+ - put on huggingface before presentation.
7
+ """
8
+
9
+
10
+ def main():
11
+ """ Run an instance of the PlaylistBuilder class developed using Gradio and Spotipy.
12
+ """
13
+ playlist_app = PlaylistBuilder()
14
+ playlist_app.launch()
15
+
16
+ if __name__ == '__main__':
17
+ main()
data/SpotifyAudioFeaturesApril2019.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6e49bb679851e58726ac74bf75ae50f5c10fce07fa9e6538ccb99a357d24648
3
+ size 20399954
data/reduced_audio_profiles.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aef94dacf666d357a0dfa9456c51e22057b66102d4e7194a3dba6b84997cd24d
3
+ size 6664964
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio
2
+ numpy
src/evolutionary_alrogithm.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import gradio as gr
4
+ import plotly.express as px
5
+ from plotly.graph_objects import Figure
6
+ from copy import deepcopy
7
+ from random import choice
8
+ from typing import List, Set
9
+ import itertools
10
+ from sklearn.preprocessing import StandardScaler
11
+ from src.spotipy_utils import SpotifyTrack
12
+
13
+ # Initial data cleaning.
14
+ SONG_DF = pd.read_csv("data/SpotifyAudioFeaturesApril2019.csv")
15
+ REDUCED_DF = pd.read_csv("data/reduced_audio_profiles.csv")
16
+ AUDIO_DF = SONG_DF.drop(["artist_name", "track_id", "track_name", "duration_ms", "key", "mode", "time_signature", "popularity"], axis=1) # TODO: Normalize these feature-wise!
17
+ AUDIO_FEATURE_NAMES = AUDIO_DF.columns
18
+ history_df = pd.DataFrame({"x": REDUCED_DF.iloc[..., 0],
19
+ "y": REDUCED_DF.iloc[..., 1],
20
+ "status": ["none" for i in range(REDUCED_DF.shape[0])],
21
+ "name": SONG_DF["track_name"].values,
22
+ "artist": SONG_DF["artist_name"].values,
23
+ "key": SONG_DF["key"].values})
24
+
25
+ # Symbols for track statuses.
26
+ THUMBS_UP = "👍"
27
+ THUMBS_DOWN = "👎"
28
+ ADD_TO_PLAYLIST = "➕ to ⏯️"
29
+ COLOR_MAP_DISCRETE = {"none": 'grey', THUMBS_UP: "blue", THUMBS_DOWN: 'red', ADD_TO_PLAYLIST: "green"}
30
+
31
+ # Normalizing audio profile features to be able to control mutation size more precisely.
32
+ scaler = StandardScaler()
33
+ audio_features = scaler.fit_transform(AUDIO_DF.values)
34
+ sample_inds = list(np.arange(audio_features.shape[0])) # index list for easy sampling
35
+
36
+ # Mutation and crossover options.
37
+ MUTATION_OPTIONS = ["None", "Random", "Differential"]
38
+ CROSSOVER_OPTIONS = ["None", "Two-Point"]
39
+
40
+
41
+ class Individual:
42
+ """ Individual representing a track in the Evolutionary algorithm population.
43
+ """
44
+ def __init__(self, song_index:int):
45
+ """Constructor for an Individual.
46
+
47
+ Args:
48
+ song_index (int): Index of the track in the dataset.
49
+ """
50
+ self.index = song_index
51
+ self.spotify_id = SONG_DF.loc[song_index, ["track_id"]][0]
52
+ self.spotify_track = SpotifyTrack(self.spotify_id) # Init a spotify track instance.
53
+ self.name = SONG_DF.loc[song_index, ["track_name"]][0]
54
+ self.artist = SONG_DF.loc[song_index, ["artist_name"]][0]
55
+ self.genome = audio_features[song_index,...] # NOTE: For now, the only genome used is the audio profile of the track.
56
+ self.status = None
57
+
58
+
59
+ def get_image_url(self) -> str:
60
+ """ Get the image for the track available on through the API.
61
+
62
+ Returns:
63
+ str: URL to the track image.
64
+ """
65
+ return self.spotify_track.track_image
66
+
67
+
68
+ def get_preview_url(self) -> str:
69
+ """ Get the preview audio for the track available on through the API.
70
+
71
+ Returns:
72
+ str: URL to the preview audio.
73
+ """
74
+ return self.spotify_track.track_preview_url
75
+
76
+
77
+ def get_track_as_block(self) -> str:
78
+ """Get track name and artist name in a string.
79
+
80
+ Returns:
81
+ str: Track name and artist name.
82
+ """
83
+ return gr.TextArea(value=f"{self.name} by {self.artist}")
84
+
85
+ def get_track_id(self) -> str:
86
+ """Get track ID for Spotify API.
87
+
88
+ Returns:
89
+ str: Spotify track ID.
90
+ """
91
+ return self.spotify_track.track_id
92
+
93
+
94
+ class Population:
95
+ """ Population of tracks to be evolved through interaction with a user in an effort to build a curated playlist.
96
+ """
97
+ def __init__(self, num_songs:int, mutation_size:float, crossover_function=None, mutation_function=None):
98
+ """ Constructor of the population initializing all the tracks in the first generation.
99
+
100
+ Args:
101
+ num_songs (int): Number of tracks in the population.
102
+ mutation_size (float): Value to scale mutations.
103
+ crossover_function (function, optional): Function to perform crossover with. Defaults to None.
104
+ mutation_function (function, optional): Function to perform mutation with. Defaults to None.
105
+ """
106
+ self.seed_track = None
107
+ self.size = num_songs
108
+ self.init_inds = np.random.choice(sample_inds, self.size)
109
+ self.pop = [Individual(ind) for ind in self.init_inds]
110
+ self.playlist_selected_songs = []
111
+ self.playlist_inds = set()
112
+ self.mutation_function = mutation_function
113
+ self.crossover_function = crossover_function
114
+ self.mutation_size = mutation_size
115
+ self.history_df = deepcopy(history_df)
116
+
117
+
118
+ def mutate(self, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
119
+ """ Method to call assigned mutation function through.
120
+
121
+ Args:
122
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
123
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
124
+ added_songs (List[Individual]): List of individuals added to the playlist.
125
+ """
126
+ self.mutation_function(self, thumbs_up, thumbs_down, added_songs)
127
+
128
+
129
+ def crossover(self, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
130
+ """ Method to call assigned crossover function through.
131
+
132
+ Args:
133
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
134
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
135
+ added_songs (List[Individual]): List of individuals added to the playlist.
136
+ """
137
+ self.crossover_function(self, thumbs_up, thumbs_down, added_songs)
138
+
139
+
140
+ def add_to_playlist(self, track:Individual):
141
+ """ Add a given track to the population's playlist.
142
+
143
+ Args:
144
+ track (Individual): Track that the user has chosen to add to the playlist.
145
+ """
146
+ self.playlist_selected_songs.append(track)
147
+ self.playlist_inds.add(track.index)
148
+
149
+
150
+ def get_tracks_with_status(self, status:str):
151
+ """ Get tracks in the current population matching some status.
152
+
153
+ Args:
154
+ status (str): Status to be matched on.
155
+ """
156
+ return list(filter(lambda x: x.status == status, self.pop))
157
+
158
+
159
+ def get_playlist_block_value(self) -> str:
160
+ """ Get population represented as a string.
161
+
162
+ Returns:
163
+ str: Tracks listed with name and artist name, one per line.
164
+ """
165
+ val_string = ""
166
+ for track in self.playlist_selected_songs:
167
+ val_string += f"{track.name} by {track.artist}\n"
168
+ return val_string
169
+
170
+
171
+ def reinitialize_pop(self):
172
+ """ Randomly re-initialize the population.
173
+ """
174
+ self.init_inds = np.random.choice(sample_inds, self.size)
175
+ self.pop = [Individual(ind) for ind in self.init_inds]
176
+
177
+
178
+ def generate_traversal_viz(self) -> Figure:
179
+ """ Generate a scatter plot marking the tracks which have been used so far and their categories.
180
+
181
+ Returns:
182
+ Figure: Plotly scatter plot.
183
+ """
184
+ return px.scatter(self.history_df, "x", "y", color="status", color_discrete_map=COLOR_MAP_DISCRETE,labels={
185
+ "x": "",
186
+ "y": "",
187
+ },)
188
+
189
+
190
+ def update_population_history(self, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
191
+ """ Update the DF keeping track of the traversal history.
192
+
193
+ Args:
194
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
195
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
196
+ added_songs (List[Individual]): List of individuals added to the playlist.
197
+ """
198
+ for track in thumbs_up:
199
+ self.history_df.at[track.index, "status"] = THUMBS_UP
200
+ for track in thumbs_down:
201
+ self.history_df.at[track.index, "status"] = THUMBS_DOWN
202
+ for track in added_songs:
203
+ self.history_df.at[track.index, "status"] = ADD_TO_PLAYLIST
204
+
205
+
206
+ def get_closest_song_to_genome(population: Population, genome: np.ndarray, new_track_inds: Set[int]) -> int:
207
+ """ Find the song in the audio profile matrix that is most similar to the supplied one.
208
+
209
+ Args:
210
+ population (Population): Population of tracks.
211
+ genome (np.ndarray): Vector of audio profile features.
212
+ new_track_inds (Set[int]): Set of the track indices being added for the new generation.
213
+
214
+ Returns:
215
+ int: Index of the most similar track in the audio matrix that is not already in the population or added to the playlist.
216
+ """
217
+ track_distances = np.mean(np.abs(genome - audio_features), axis=1)
218
+ sorted_distance_inds = np.argsort(track_distances)
219
+ current_distance_index = 0
220
+ while sorted_distance_inds[current_distance_index] in new_track_inds or sorted_distance_inds[current_distance_index] in population.playlist_inds:
221
+ current_distance_index += 1
222
+
223
+ child_index = sorted_distance_inds[current_distance_index]
224
+ child = Individual(child_index)
225
+ new_track_inds.add(child_index)
226
+ return child
227
+
228
+
229
+ def simple_crossover(population: Population, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
230
+ """ Performs simple two point crossover.
231
+
232
+ Args:
233
+ population (Population): Population to assign new children to.
234
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
235
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
236
+ added_songs (List[Individual]): List of individuals added to the playlist.
237
+ """
238
+ liked_tracks = list(itertools.chain(*[thumbs_up, added_songs]))
239
+ if len(liked_tracks) < 2: # Not enough tracks to perform crossover.
240
+ return
241
+
242
+ new_track_inds = set()
243
+ population_index = 0
244
+ while len(new_track_inds) < population.size:
245
+ track_a, track_b = np.random.choice(liked_tracks, 2)
246
+ crossover_point1, crossover_point2 = sorted(np.random.randint(0,len(track_a.genome),2))
247
+
248
+ track_a.genome[crossover_point1:crossover_point2+1] = track_b.genome[crossover_point1:crossover_point2+1]
249
+ track_b.genome[crossover_point1:crossover_point2+1] = track_a.genome[crossover_point1:crossover_point2+1]
250
+
251
+ child1 = get_closest_song_to_genome(population, track_a.genome, new_track_inds)
252
+ population.pop[population_index] = child1
253
+ population_index += 1
254
+
255
+ if population_index < population.size:
256
+ child2 = get_closest_song_to_genome(population, track_b.genome, new_track_inds)
257
+ population.pop[population_index] = child2
258
+ population_index += 1
259
+
260
+
261
+ def simple_mutation(population: Population, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
262
+ """ Perform simple random mutation on a population of individuals.
263
+
264
+ Args:
265
+ population (Population): Population to assign new children to.
266
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
267
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
268
+ added_songs (List[Individual]): List of individuals added to the playlist.
269
+ """
270
+ liked_tracks = list(itertools.chain(*[thumbs_up, added_songs]))
271
+ new_track_inds = set()
272
+ for i in range(population.size):
273
+ liked_track = choice(liked_tracks)
274
+ mutated_track_genome = liked_track.genome + np.random.uniform(size=(population.size))*population.mutation_size
275
+ child = get_closest_song_to_genome(population, mutated_track_genome, new_track_inds)
276
+ population.pop[i] = child
277
+
278
+
279
+ def differential_mutation(population: Population, thumbs_up:List[Individual], thumbs_down:List[Individual], added_songs:List[Individual]):
280
+ """ Perform differential mutation based on the user's decisions.
281
+
282
+ Args:
283
+ population (Population): Population to assign new children to.
284
+ thumbs_up (List[Individual]): List of individuals given the thumbs up.
285
+ thumbs_down (List[Individual]): List of individuals given the thumbs down.
286
+ added_songs (List[Individual]): List of individuals added to the playlist.
287
+ """
288
+ new_track_inds = set()
289
+ for i in range(population.size):
290
+ if thumbs_down:
291
+ disliked_track = choice(thumbs_down)
292
+ else:
293
+ disliked_track = Individual(np.random.choice(sample_inds, 1)[0])
294
+
295
+ if thumbs_up:
296
+ seed_track = choice(thumbs_up)
297
+ else:
298
+ seed_track = Individual(np.random.choice(sample_inds, 1)[0])
299
+
300
+ if added_songs:
301
+ attracting_track = choice(added_songs)
302
+ elif population.playlist_selected_songs:
303
+ attracting_track = choice(population.playlist_selected_songs)
304
+ else:
305
+ attracting_track = Individual(np.random.choice(sample_inds, 1)[0])
306
+
307
+ gene_distances = attracting_track.genome - disliked_track.genome
308
+ resulting_genome = seed_track.genome + gene_distances*population.mutation_size
309
+ child = get_closest_song_to_genome(population, resulting_genome, new_track_inds)
310
+ population.pop[i] = child
src/playlist_builder.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from src.evolutionary_alrogithm import Population, simple_mutation, simple_crossover, differential_mutation, MUTATION_OPTIONS, CROSSOVER_OPTIONS, \
3
+ THUMBS_UP, THUMBS_DOWN, ADD_TO_PLAYLIST
4
+ from src.spotipy_utils import add_tracks_to_playlist, get_auth_manager, search_for_track
5
+
6
+ # IMGs
7
+ ADDED_TO_PLAYLIST = "✅ Added to Library!"
8
+ FAILED_TO_ADD = """❌ Failed to connect to your account. Make sure to that you clicked the link in the description to login and use the app from the window that opens there.\n
9
+ Also check to make sure have entered a valid playlist name and have reached out to the listed email to be added to the user list."""
10
+
11
+ # Evolutionary Algorithm Parameters.
12
+ NUM_SONGS = 9
13
+ DEFAULT_MUTATION_SIZE = 0.25 # NOTE: This could be turned into a parameter which is set by a slider.
14
+ SONG_CHOICES = [THUMBS_UP, THUMBS_DOWN, ADD_TO_PLAYLIST]
15
+ SEED_OPTIONS = ["yes", "no"]
16
+
17
+ APP_HEADER = """# Evolutionary Playlist Builder"""
18
+ OPTIONS_TITLE = """# 🧬 Options"""
19
+ EXPLORATION_HEADER = """## Rate of Exploration"""
20
+ PLAYLIST_HEADER = """## Current Playlist"""
21
+
22
+
23
+ def change_seed_option(seed_option):
24
+ if seed_option == SEED_OPTIONS[0]:
25
+ return gr.Textbox.update(visible=True), gr.Dropdown.update(visible=True)
26
+ else:
27
+ return gr.Textbox.update(visible=False), gr.Dropdown.update(visible=False)
28
+
29
+
30
+ class PlaylistBuilder():
31
+ """Evolutionary interactive playlist builder via the Spotify API, Gradio, and a static dataset of audio profile features.
32
+ """
33
+ def __init__(self):
34
+ self.demo = gr.Blocks()
35
+ with self.demo:
36
+ gr.Markdown(APP_HEADER)
37
+
38
+ with gr.Row():
39
+ # Initialize app attributes
40
+ self.do_crossover = False
41
+ self.do_mutation = False
42
+ self.population = Population(NUM_SONGS, DEFAULT_MUTATION_SIZE, mutation_function=simple_mutation, crossover_function=simple_crossover)
43
+
44
+ # Generate blank traversal visualization.
45
+ initial_traversal_history = self.population.generate_traversal_viz()
46
+
47
+ # Layout first column.
48
+ with gr.Column():
49
+ auth_manager = get_auth_manager() # Need auth manager to generate Spotify API authentication url.
50
+ gr.Markdown(
51
+ f"""
52
+ Please click this link to login: [login]({auth_manager.get_authorize_url()})\n
53
+ This app is still in development mode so if you want the ability to add a playlist to your library, email carterward4@gmail.com with your name, spotify email, and the subject as PLAYLIST ACCESS REQUEST.
54
+ """)
55
+
56
+ gr.Markdown(OPTIONS_TITLE)
57
+
58
+ crossover_radio = gr.Radio(choices=CROSSOVER_OPTIONS, value=CROSSOVER_OPTIONS[0], label="Crossover?") # radio button for crossover options.
59
+
60
+ mutation_radio = gr.Radio(choices=MUTATION_OPTIONS, value=MUTATION_OPTIONS[0], label="Mutate?") # radio button for mutation options.
61
+
62
+ gr.Markdown(EXPLORATION_HEADER, visible=False)
63
+
64
+ mutation_size_slider = gr.Slider(minimum=0.0, maximum=1.0, value=DEFAULT_MUTATION_SIZE
65
+ , interactive=True, show_label=False, visible=False) # slider controlling mutation slide. Values restricted between 0 and 1.
66
+
67
+ history_block = gr.Plot(initial_traversal_history, label="Search History") # Plot block for blank viz.
68
+
69
+ gr.Markdown(PLAYLIST_HEADER)
70
+
71
+ playlist_display = gr.TextArea(value=self.population.get_playlist_block_value(), max_lines=10000, show_label=False) # Blank playlist display.
72
+
73
+ spotify_playlist_textbox = gr.Textbox(interactive=True, label="Enter Playlist Name Here") # Username entry.
74
+ add_to_library = gr.Button(value="Add Playlist to Spotify Library") # Button to add to playlist
75
+ playlist_added_message = gr.Markdown(ADDED_TO_PLAYLIST, visible=False)
76
+
77
+ # Build grid layout with initial song art and options.
78
+ index_blocks = []
79
+ name_blocks = []
80
+ image_blocks = []
81
+ dropdown_blocks = []
82
+ preview_blocks = []
83
+
84
+ with gr.Column():
85
+ # Init seed options
86
+ seed_option = gr.Radio(SEED_OPTIONS, value=SEED_OPTIONS[-1], label="Seed generation?", show_label=True)
87
+ seed_search = gr.Textbox(interactive=True, label="Search for seed track by name", show_label=True, visible=False)
88
+ search_results = gr.Dropdown(label="Search result options", visible=False, interactive=True)
89
+
90
+ # Init population grid.
91
+ with gr.Box():
92
+ for row in range(0, self.population.size, 3): # Iterate over track rows.
93
+ with gr.Row():
94
+ for col in range(3): # Iterate over track columns.
95
+ with gr.Box():
96
+ track_index = row + col
97
+ current_song = self.population.pop[track_index]
98
+ song_preview = current_song.get_preview_url()
99
+
100
+ index_blocks.append(gr.TextArea(value=track_index, visible=False)) # Init index blocks to keep track of everything.
101
+ # Render initial images.
102
+ image_blocks.append(gr.Image(value=current_song.get_image_url(), show_label=False, visible=True).style(height=175, width=175))
103
+ # Add names.
104
+ name_blocks.append(gr.Markdown(value=f"{current_song.name} by {current_song.artist}", show_label=False))
105
+ # Dropdown for song options.
106
+ dropdown_blocks.append(gr.Dropdown(SONG_CHOICES, label = track_index, show_label=False))
107
+ # Add song preview if it exists.
108
+ if song_preview:
109
+ preview_blocks.append(gr.Audio(value=song_preview, show_label=False))
110
+ else:
111
+ preview_blocks.append(gr.Audio(value=song_preview, show_label=False, visible=False))
112
+
113
+ next_generation = gr.Button(value="Next Generation").style(full_width=True)
114
+
115
+ # Mapping change functions to class methods.
116
+
117
+ crossover_radio.change(
118
+ fn=self.set_crossover_option,
119
+ inputs=[crossover_radio]
120
+ )
121
+
122
+ mutation_radio.change(
123
+ fn=self.set_mutation_option,
124
+ inputs=[mutation_radio],
125
+ outputs=[mutation_size_slider, crossover_radio]
126
+ )
127
+
128
+ mutation_size_slider.change(
129
+ fn=self.update_mutation_size,
130
+ inputs=[mutation_size_slider]
131
+ )
132
+
133
+
134
+ seed_option.change(
135
+ fn=change_seed_option,
136
+ inputs=[seed_option],
137
+ outputs=[seed_search, search_results]
138
+ )
139
+
140
+ seed_search.change(
141
+ fn=self.search_for_seed,
142
+ inputs=[seed_search],
143
+ outputs=search_results
144
+ )
145
+
146
+ # Set function to hangle change in dropdown value for each track.
147
+ for index_block, dropdown_block in zip(index_blocks, dropdown_blocks):
148
+ dropdown_block.change(
149
+ fn=self.set_track_state,
150
+ inputs=[index_block, dropdown_block]
151
+ )
152
+
153
+ # Set function to generate next generation of tracks.
154
+ next_generation.click(
155
+ fn=self.get_next_generation,
156
+ outputs=[playlist_display, *image_blocks, *name_blocks, *dropdown_blocks, *preview_blocks, history_block]
157
+ )
158
+
159
+ add_to_library.click(
160
+ fn=self.add_playlist_to_spotify,
161
+ inputs=[spotify_playlist_textbox],
162
+ outputs=[playlist_added_message]
163
+ )
164
+
165
+
166
+ def set_crossover_option(self, radio_selection:str):
167
+ """ Set the crossover option for the internal evolutionary algorithm.
168
+
169
+ Args:
170
+ radio_selection (str): Selected crossover option.
171
+ """
172
+ if radio_selection == CROSSOVER_OPTIONS[0]:
173
+ self.do_crossover = False
174
+ else:
175
+ self.do_crossover = True
176
+
177
+
178
+ def set_mutation_option(self, radio_selection:str) -> bool:
179
+ """ Set the mutation option for the internal evolutionary algorithm.
180
+
181
+ Args:
182
+ radio_selection (str): Selected mutation option.
183
+
184
+ Returns:
185
+ tuple: Slider update for mutation rate and Radio update for crossover options.
186
+ """
187
+ if radio_selection == MUTATION_OPTIONS[0]:
188
+ self.do_mutation = False
189
+ updated_slider = gr.Slider.update(visible=False)
190
+ updated_crossover_button = gr.Radio.update(visible=True)
191
+ elif radio_selection == MUTATION_OPTIONS[2]:
192
+ self.do_mutation = True
193
+ updated_slider = gr.Slider.update(visible=True)
194
+ updated_crossover_button = gr.Radio.update(visible=False, value=CROSSOVER_OPTIONS[0])
195
+
196
+ self.do_crossover = False
197
+ self.population.mutation_function = differential_mutation
198
+ else:
199
+ self.do_mutation = True
200
+ updated_slider = gr.Slider.update(visible=True)
201
+ updated_crossover_button = gr.Radio.update(visible=True)
202
+
203
+ return updated_slider, updated_crossover_button
204
+
205
+
206
+ def update_mutation_size(self, mutation_size_slider_value:str):
207
+ """Update the mutation rate according the slider value.
208
+
209
+ Args:
210
+ mutation_size_slider_value (str): Value from the slider input.
211
+ """
212
+ self.population.mutation_size = float(mutation_size_slider_value)
213
+
214
+
215
+ def search_for_seed(self, search_term):
216
+ search_for_track(search_term)
217
+ return gr.Dropdown.update(choices=[f"Test {i}" for i in range(self.population.size)])
218
+
219
+
220
+ def set_track_state(self, track_index:str, dropdown_value:str):
221
+ """Sets the decision status of the track for the corresponding dropdown menu.
222
+
223
+ Args:
224
+ track_index (str): Index of the track in the population array.
225
+ dropdown_value (str): Value selected from the track's decision dropdown.
226
+ """
227
+ current_track = self.population.pop[int(track_index)]
228
+ current_track.status = dropdown_value
229
+
230
+
231
+ def get_next_generation(self):
232
+ """ Updates the display grid, the playlist display, and the historical tracking vizualization for the next generation.
233
+
234
+ Returns:
235
+ tuple: Current playlist display, track image, name, option, and preview blocks, and tracking history visualization.
236
+ """
237
+ thumbs_up = self.population.get_tracks_with_status(THUMBS_UP)
238
+ thumbs_down = self.population.get_tracks_with_status(THUMBS_DOWN)
239
+ added_songs = self.population.get_tracks_with_status(ADD_TO_PLAYLIST)
240
+
241
+ for added_track in added_songs:
242
+ self.population.add_to_playlist(added_track)
243
+
244
+ playlist_display = gr.TextArea.update(
245
+ value=self.population.get_playlist_block_value()
246
+ )
247
+
248
+ # HANDLING FOR IF NO SONGS ARE SELECTED!
249
+ if not thumbs_up and not added_songs:
250
+ self.population.reinitialize_pop()
251
+ else:
252
+ if self.do_crossover and self.do_mutation:
253
+ self.population.crossover(thumbs_up, thumbs_down, added_songs)
254
+ self.population.mutate(self.population.pop, [], [])
255
+ elif self.do_crossover:
256
+ self.population.crossover(thumbs_up, thumbs_down, added_songs)
257
+ elif self.do_mutation:
258
+ self.population.mutate(thumbs_up, thumbs_down, added_songs)
259
+
260
+ # GET IMAGES AND PREVIEWS FOR NEW POPULATION.
261
+ image_blocks = []
262
+ name_blocks = []
263
+ preview_blocks = []
264
+ dropdown_blocks = []
265
+ for track in self.population.pop:
266
+ image_blocks.append(gr.Image.update(value=track.get_image_url()))
267
+ name_blocks.append(gr.Markdown.update(value=f"{track.name} by {track.artist}"))
268
+ dropdown_blocks.append(gr.Dropdown.update(value=None))
269
+ song_preview = track.get_preview_url()
270
+ if song_preview:
271
+ preview_blocks.append(gr.Audio.update(value=song_preview, visible=True))
272
+ else:
273
+ preview_blocks.append(gr.Audio.update(value=song_preview, visible=False))
274
+
275
+ # Update historical df and traversal visualization.
276
+ self.population.update_population_history(thumbs_up, thumbs_down, added_songs)
277
+ updated_viz = self.population.generate_traversal_viz()
278
+ new_traversal_tracker = gr.Plot.update(updated_viz)
279
+
280
+ return (playlist_display, *image_blocks, *name_blocks, *dropdown_blocks, *preview_blocks, new_traversal_tracker)
281
+
282
+
283
+ def add_playlist_to_spotify(self, playlist_name:str, request:gr.Request):
284
+ """Add curated playlist to authenticated user's playlist library. Playlist will be added as public.
285
+
286
+ Args:
287
+ playlist_name (str): Name of the playlist to be created.
288
+ request (gr.Request): Request information containing the required key for user authentication.
289
+
290
+ Returns:
291
+ gr.Markdown.update: Message displaying the success of the attempt to add to library.
292
+ """
293
+ code = request.headers.get("referer").split("code=")[-1]
294
+ added = add_tracks_to_playlist(code, playlist_name, self.population.playlist_selected_songs)
295
+
296
+ if not added:
297
+ return gr.Markdown.update(FAILED_TO_ADD, visible=True)
298
+
299
+ return gr.Markdown.update(ADDED_TO_PLAYLIST, visible=True)
300
+
301
+
302
+ def launch(self, share=False):
303
+ """Launch the Gradio app.
304
+
305
+ Args:
306
+ share (bool, optional): Whether or not a temporary publicly accessible link should be generated. Defaults to False.
307
+ """
308
+ self.demo.launch(share=share)
src/spotipy_utils.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spotipy
2
+ from spotipy.oauth2 import SpotifyClientCredentials
3
+ from typing import List
4
+ from src.config.spotify_credentials import CLIENT_ID, CLIENT_SECRET # TODO: Make these environment variables in hugging face space.
5
+
6
+ PLAYLIST_SCOPE = 'playlist-modify-public'
7
+ REDIRECT_URL = "http://127.0.0.1:7860"
8
+
9
+ sp_client = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(CLIENT_ID, CLIENT_SECRET))
10
+
11
+
12
+ class SpotifyTrack:
13
+ """ Object for keeping track of the information provided through the Spotify API on a particular track.
14
+ """
15
+ def __init__(self, track_id: str) -> None:
16
+ """ Constructor for class.
17
+
18
+ Args:
19
+ track_id (str): ID of the track on the Spotify API.
20
+ """
21
+ self.track_id = track_id
22
+ self.raw_track_info = sp_client.track(track_id)
23
+ self.track_image = self.raw_track_info["album"]["images"][0]["url"]
24
+ self.track_preview_url = self.raw_track_info["preview_url"]
25
+
26
+
27
+ def get_auth_manager() -> spotipy.oauth2.SpotifyOAuth:
28
+ """ Generate authentication manager object.
29
+
30
+ Returns:
31
+ spotipy.oauth2.SpotifyOAuth: Spotipy authentication manager.
32
+ """
33
+ cache_handler = spotipy.cache_handler.CacheFileHandler()
34
+ return spotipy.oauth2.SpotifyOAuth(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, scope=PLAYLIST_SCOPE, redirect_uri=REDIRECT_URL,
35
+ cache_handler=cache_handler,
36
+ show_dialog=True)
37
+
38
+
39
+ def search_for_track(track_name):
40
+ search_results = sp_client.search(q = "track:" + track_name, type = "track", limit=10)
41
+ for track_info in search_results["tracks"]["items"]:
42
+ print(track_info["name"])
43
+
44
+
45
+ def add_tracks_to_playlist(code:str, playlist_id:str, tracks:List) -> bool:
46
+ """ Add tracks to a given user's Spotify Library.
47
+
48
+ Args:
49
+ code (str): Code for authentication.
50
+ playlist_id (str): Name of the playlist to be created.
51
+ tracks (List): List of the tracks to be added to the playlist.
52
+
53
+ Returns:
54
+ bool: Flag indicating whether or not the playlist was added to the library succesfully.
55
+ """
56
+ auth_manager = get_auth_manager()
57
+ try:
58
+ auth_manager.get_access_token(code)
59
+ except spotipy.oauth2.SpotifyOauthError as e:
60
+ print(e)
61
+ return False
62
+
63
+ user_client = spotipy.Spotify(auth_manager=auth_manager)
64
+ username = user_client.current_user()["id"]
65
+
66
+ try:
67
+ playlist_result = user_client.user_playlist_create(username, playlist_id, public=True)
68
+ except spotipy.exceptions.SpotifyException as e:
69
+ print(e)
70
+ return False
71
+
72
+ playlist_id = playlist_result["id"]
73
+ tracks_to_add = ["spotify:track:" + track.get_track_id() for track in tracks]
74
+ if tracks_to_add:
75
+ user_client.user_playlist_add_tracks(username, playlist_id, tracks_to_add)
76
+ return True