sjw commited on
Commit
494ea9a
1 Parent(s): 662bfbf

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +844 -0
  2. messages.py +39 -0
  3. requirements.txt +14 -0
app.py ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Standard Library Imports
2
+ import os
3
+ import random
4
+ import re
5
+ import time
6
+ from urllib.parse import urlparse, parse_qs
7
+
8
+ # Third-Party Imports
9
+ import gradio as gr
10
+ import lyricsgenius
11
+ import requests
12
+ import spotipy
13
+ from bs4 import BeautifulSoup
14
+ from dotenv import load_dotenv
15
+ from fuzzywuzzy import fuzz
16
+ from pydantic import BaseModel, Field
17
+ from requests.exceptions import Timeout
18
+ from sentence_transformers import SentenceTransformer
19
+ from sklearn.metrics.pairwise import cosine_similarity
20
+ from spotipy.exceptions import SpotifyException
21
+
22
+ # Local Application/Library Specific Imports
23
+ from langchain.agents import OpenAIFunctionsAgent, AgentExecutor, tool
24
+ from langchain.chat_models import ChatOpenAI
25
+ from langchain.memory import ConversationBufferMemory
26
+ from langchain.prompts import MessagesPlaceholder
27
+ from langchain.schema import SystemMessage, HumanMessage
28
+ from messages import SYSTEM_MESSAGE, GENRE_LIST
29
+
30
+ from dotenv import load_dotenv
31
+ load_dotenv()
32
+
33
+
34
+ # ------------------------------
35
+ # Section: Global Vars
36
+ # ------------------------------
37
+
38
+
39
+ GENIUS_TOKEN = os.getenv("GENIUS_ACCESS_TOKEN")
40
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
41
+
42
+ DEBUG_MODE = True
43
+ def debug_print(*args, **kwargs):
44
+ if DEBUG_MODE:
45
+ print(*args, **kwargs)
46
+
47
+ THEME = gr.themes.Default(
48
+ primary_hue=gr.themes.colors.red,
49
+ secondary_hue=gr.themes.colors.pink,
50
+ font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"],
51
+ spacing_size=gr.themes.sizes.spacing_sm,
52
+ radius_size=gr.themes.sizes.radius_sm
53
+ )#.set(body_background_fill="#FFFFFF")
54
+
55
+ # TODO: switch to personal website
56
+ REDIRECT_URI = "https://huggingface.co/sjw"
57
+
58
+ # Spotify functions
59
+ SCOPE = [
60
+ 'user-library-read',
61
+ 'user-read-playback-state',
62
+ 'user-modify-playback-state',
63
+ 'playlist-modify-public',
64
+ 'user-top-read'
65
+ ]
66
+
67
+ MOOD_SETTINGS = {
68
+ "happy": {"max_instrumentalness": 0.001, "min_valence": 0.6},
69
+ "sad": {"max_danceability": 0.65, "max_valence": 0.4},
70
+ "energetic": {"min_tempo": 120, "min_danceability": 0.75},
71
+ "calm": {"max_energy": 0.65, "max_tempo": 130}
72
+ }
73
+
74
+ # genre + mood function
75
+ NUM_ARTISTS = 20 # artists to retrieve from user's top artists
76
+ TIME_RANGE = "medium_term" # short, medium, long
77
+ NUM_TRACKS = 10 # tracks to add to playback
78
+ MAX_ARTISTS = 4 # sp.recommendations() seeds: 4/5 artists, 1/5 genre
79
+
80
+ # artist + mood function
81
+ NUM_ALBUMS = 20 # maximum number of albums to retrieve from an artist
82
+ MAX_TRACKS = 10 # tracks to randomly select from an artist
83
+
84
+ # matching playlists + moods
85
+ MODEL = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') # smaller BERT
86
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # warning
87
+ MOOD_LIST = ["happy", "sad", "energetic", "calm"]
88
+ MOOD_EMBEDDINGS = MODEL.encode(MOOD_LIST)
89
+ GENRE_EMBEDDINGS = MODEL.encode(GENRE_LIST)
90
+
91
+ # agent tools
92
+ RETURN_DIRECT = True
93
+
94
+ MODEL = "gpt-3.5-turbo-0613"
95
+
96
+ THEMES = ["Epic", "Hypnotic", "Dreamy", "Legendary", "Majestic",
97
+ "Enchanting", "Ethereal", "Super Lit", "Harmonious", "Heroic"]
98
+
99
+
100
+ with gr.Blocks(theme=THEME) as auth_page:
101
+
102
+ # ------------------------------
103
+ # Section: Spotify Authentication
104
+ # ------------------------------
105
+
106
+
107
+ ACCESS_TOKEN_VAR = gr.State()
108
+ AGENT_EXECUTOR_VAR = gr.State()
109
+
110
+
111
+ client_id = gr.Textbox(placeholder="5. Paste Spotify Client ID here, then click the button below", container=False, text_align="center")
112
+ generate_link = gr.Button("6. Get Authentication Link")
113
+ display_link = gr.Markdown()
114
+ url = gr.Textbox(placeholder="7. Paste entire URL here, then click the button below", container=False, text_align="center")
115
+ authorize_url = gr.Button("8. Authorize URL")
116
+ auth_result = gr.Markdown()
117
+
118
+
119
+ def spotify_auth(client_id, url=None, access_tokens=None):
120
+ """
121
+ Authenticate Spotify with the provided client_id and url.
122
+ """
123
+ if url:
124
+ parsed_url = urlparse(url)
125
+ fragment = parsed_url.fragment
126
+ access_token = parse_qs(fragment)['access_token'][0]
127
+ print(access_token)
128
+
129
+ return access_token, """<span style="font-size:18px;">Authentication Success.</span>"""
130
+
131
+ else:
132
+ auth_url = (
133
+ f"https://accounts.spotify.com/authorize?response_type=token&client_id={client_id}"
134
+ f"&scope={'%20'.join(SCOPE)}&redirect_uri={REDIRECT_URI}"
135
+ )
136
+
137
+ return {
138
+ display_link: ("""<span style="font-size:18px;">Authorize by clicking <strong><a href='""" + f"{auth_url}" +
139
+ """' target="_blank">here</a></strong> and copy the '<strong>entire URL</strong>' you are redirected to</span>""")
140
+ }
141
+
142
+
143
+ generate_link.click(spotify_auth, inputs=[client_id], outputs=display_link)
144
+ authorize_url.click(spotify_auth, inputs=[client_id, url, ACCESS_TOKEN_VAR], outputs=[ACCESS_TOKEN_VAR, auth_result])
145
+
146
+ create_agent_button = gr.Button("Create Apollo")
147
+
148
+ def create_agent(access_token):
149
+
150
+
151
+ # ------------------------------
152
+ # Section: Spotify Functions
153
+ # ------------------------------
154
+
155
+
156
+ sp = spotipy.Spotify(auth=access_token)
157
+ device_id = sp.devices()['devices'][0]['id']
158
+
159
+
160
+ def find_track_by_name(track_name):
161
+ """
162
+ Finds the Spotify track URI given the track name.
163
+ """
164
+ results = sp.search(q=track_name, type='track')
165
+ track_uri = results['tracks']['items'][0]['uri']
166
+ return track_uri
167
+
168
+
169
+ def play_track_by_name(track_name):
170
+ """
171
+ Plays a track given its name. Uses the above function.
172
+ """
173
+ track_uri = find_track_by_name(track_name)
174
+ track_name = sp.track(track_uri)["name"]
175
+ artist_name = sp.track(track_uri)['artists'][0]['name']
176
+
177
+ try:
178
+ sp.start_playback(device_id=device_id, uris=[track_uri])
179
+ return f"♫ Now playing {track_name} by {artist_name} ♫"
180
+ except SpotifyException as e:
181
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
182
+ except Exception as e:
183
+ return f"An unexpected error occurred: {e}."
184
+
185
+
186
+ def queue_track_by_name(track_name):
187
+ """
188
+ Queues track given its name.
189
+ """
190
+ track_uri = find_track_by_name(track_name)
191
+ track_name = sp.track(track_uri)["name"]
192
+ sp.add_to_queue(uri=track_uri, device_id=device_id.value)
193
+ return f"♫ Added {track_name} to your queue ♫"
194
+
195
+
196
+ def pause_track():
197
+ """
198
+ Pauses the current playback.
199
+ """
200
+ sp.pause_playback(device_id=device_id.value)
201
+ return "♫ Playback paused ♫"
202
+
203
+
204
+ def resume_track():
205
+ """
206
+ Resumes the current playback.
207
+ """
208
+ sp.start_playback(device_id=device_id.value)
209
+ return "♫ Playback started ♫"
210
+
211
+
212
+ def skip_track():
213
+ """
214
+ Skips the current playback.
215
+ """
216
+ sp.next_track(device_id=device_id.value)
217
+ return "♫ Skipped to your next track ♫"
218
+
219
+
220
+ ### ### ### More Elaborate Functions ### ### ###
221
+
222
+
223
+ def play_album_by_name_and_artist(album_name, artist_name):
224
+ """
225
+ Plays an album given its name and the artist.
226
+ context_uri (provide a context_uri to start playback of an album, artist, or playlist) expects a string.
227
+ """
228
+ results = sp.search(q=f'{album_name} {artist_name}', type='album')
229
+ album_id = results['albums']['items'][0]['id']
230
+ album_info = sp.album(album_id)
231
+ album_name = album_info['name']
232
+ artist_name = album_info['artists'][0]['name']
233
+
234
+ try:
235
+ sp.start_playback(device_id=device_id.value, context_uri=f'spotify:album:{album_id}')
236
+ return f"♫ Now playing {album_name} by {artist_name} ♫"
237
+ except spotipy.SpotifyException as e:
238
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
239
+ except Timeout:
240
+ return f"An unexpected error occurred: {e}."
241
+
242
+
243
+ def play_playlist_by_name(playlist_name):
244
+ """
245
+ Plays an existing playlist in the user's library given its name.
246
+ """
247
+ playlists = sp.current_user_playlists()
248
+ playlist_dict = {playlist['name']: (playlist['id'], playlist['owner']['display_name']) for playlist in playlists['items']}
249
+ playlist_names = [key for key in playlist_dict.keys()]
250
+
251
+ # defined inside to capture user-specific playlists
252
+ playlist_name_embeddings = MODEL.encode(playlist_names)
253
+ user_playlist_embedding = MODEL.encode([playlist_name])
254
+
255
+ # compares (embedded) given name to (embedded) playlist library and outputs the closest match
256
+ similarity_scores = cosine_similarity(user_playlist_embedding, playlist_name_embeddings)
257
+ most_similar_index = similarity_scores.argmax()
258
+ playlist_name = playlist_names[most_similar_index]
259
+
260
+ try:
261
+ playlist_id, creator_name = playlist_dict[playlist_name]
262
+ sp.start_playback(device_id=device_id.value, context_uri=f'spotify:playlist:{playlist_id}')
263
+ return f'♫ Now playing {playlist_name} by {creator_name} ♫'
264
+ except:
265
+ return "Unable to find playlist. Please try again."
266
+
267
+
268
+ def get_track_info():
269
+ """
270
+ Harvests information for explain_track() using Genius' API and basic webscraping.
271
+ """
272
+ current_track_item = sp.current_user_playing_track()['item']
273
+ track_name = current_track_item['name']
274
+ artist_name = current_track_item['artists'][0]['name']
275
+ album_name = current_track_item['album']['name']
276
+ release_date = current_track_item['album']['release_date']
277
+ basic_info = {
278
+ 'track_name': track_name,
279
+ 'artist_name': artist_name,
280
+ 'album_name': album_name,
281
+ 'release_date': release_date,
282
+ }
283
+
284
+ # define inside to avoid user conflicts (simultaneously query Genius)
285
+ genius = lyricsgenius.Genius(GENIUS_TOKEN)
286
+ # removing feature information from song titles to avoid scewing search
287
+ track_name = re.split(' \(with | \(feat\. ', track_name)[0]
288
+ result = genius.search_song(track_name, artist_name)
289
+
290
+ # if no Genius page exists
291
+ if result is not None and hasattr(result, 'artist'):
292
+ genius_artist = result.artist.lower().replace(" ", "")
293
+ spotify_artist = artist_name.lower().replace(" ", "")
294
+ debug_print(spotify_artist)
295
+ debug_print(genius_artist)
296
+ if spotify_artist not in genius_artist:
297
+ return basic_info, None, None, None
298
+ else:
299
+ genius_artist = None
300
+ return basic_info, None, None, None
301
+
302
+ # if Genius page exists
303
+ lyrics = result.lyrics
304
+ url = result.url
305
+ response = requests.get(url)
306
+
307
+ # parsing the webpage and locating 'About' section
308
+ soup = BeautifulSoup(response.text, 'html.parser')
309
+ # universal 'About' section element across all Genius song lyrics pages
310
+ about_section = soup.select_one('div[class^="RichText__Container-oz284w-0"]')
311
+
312
+ # if no 'About' section exists
313
+ if not about_section:
314
+ return basic_info, None, lyrics, url
315
+
316
+ # if 'About' section exists
317
+ else:
318
+ about_section = about_section.get_text(separator='\n')
319
+ return basic_info, about_section, lyrics, url
320
+
321
+
322
+ def explain_track():
323
+ """
324
+ Displays track information in an organized, informational, and compelling manner.
325
+ Uses the above function.
326
+ """
327
+ # defined inside to avoid circular importing
328
+ from final_agent import LLM_STATE
329
+
330
+ basic_info, about_section, lyrics, url = get_track_info()
331
+ debug_print(basic_info, about_section, lyrics, url)
332
+
333
+ if lyrics: # if Genius page exists
334
+ system_message_content = """
335
+ Your task is to create an engaging summary for a track using the available details
336
+ about the track and its lyrics. If there's insufficient or no additional information
337
+ besides the lyrics, craft the entire summary based solely on the lyrical content."
338
+ """
339
+ human_message_content = f"{about_section}\n\n{lyrics}"
340
+ messages = [
341
+ SystemMessage(content=system_message_content),
342
+ HumanMessage(content=human_message_content)
343
+ ]
344
+ ai_response = LLM_STATE.value(messages).content
345
+ summary = f"""
346
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
347
+ **Artist:** {basic_info["artist_name"]}
348
+ **Album:** {basic_info["album_name"]}
349
+ **Release:** {basic_info["release_date"]}
350
+
351
+ **About:**
352
+ {ai_response}
353
+
354
+ <a href='{url}'>Click here for more information on Genius!</a>
355
+ """
356
+ return summary
357
+
358
+ else: # if no Genius page exists
359
+ url = "https://genius.com/Genius-how-to-add-songs-to-genius-annotated"
360
+ summary = f"""
361
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
362
+ **Artist:** {basic_info["artist_name"]}
363
+ **Album:** {basic_info["album_name"]}
364
+ **Release:** {basic_info["release_date"]}
365
+
366
+ **About:**
367
+ Unfortunately, this track has not been uploaded to Genius.com
368
+
369
+ <a href='{url}'>Be the first to change that!</a>
370
+ """
371
+ return summary
372
+
373
+
374
+ ### ### ### Genre + Mood ### ### ###
375
+
376
+
377
+ def get_user_mood(user_mood):
378
+ """
379
+ Categorizes the user's mood as either 'happy', 'sad', 'energetic', or 'calm'.
380
+ Uses same cosine similarity/embedding concepts as with determining playlist names.
381
+ """
382
+ if user_mood.lower() in MOOD_LIST:
383
+ user_mood = user_mood.lower()
384
+ return user_mood
385
+ else:
386
+ user_mood_embedding = MODEL.encode([user_mood.lower()])
387
+ similarity_scores = cosine_similarity(user_mood_embedding, MOOD_EMBEDDINGS)
388
+ most_similar_index = similarity_scores.argmax()
389
+ user_mood = MOOD_LIST[most_similar_index]
390
+ return user_mood
391
+
392
+
393
+ def get_genre_by_name(genre_name):
394
+ """
395
+ Matches user's desired genre to closest (most similar) existing genre in the list of genres.
396
+ recommendations() only accepts genres from this list.
397
+ """
398
+ if genre_name.lower() in GENRE_LIST.value:
399
+ genre_name = genre_name.lower()
400
+ return genre_name
401
+ else:
402
+ genre_name_embedding = MODEL.encode([genre_name.lower()])
403
+ similarity_scores = cosine_similarity(genre_name_embedding, GENRE_EMBEDDINGS.value)
404
+ most_similar_index = similarity_scores.argmax()
405
+ genre_name = GENRE_LIST.value[most_similar_index]
406
+ return genre_name
407
+
408
+
409
+ def is_genre_match(genre1, genre2, threshold=75):
410
+ """
411
+ Determines if two genres are semantically similar.
412
+ token_set_ratio() - for quantifying semantic similarity - and
413
+ threshold of 75 (out of 100) were were arbitrarily determined through basic testing.
414
+ """
415
+ score = fuzz.token_set_ratio(genre1, genre2)
416
+ debug_print(score)
417
+ return score >= threshold
418
+
419
+
420
+ def create_track_list_str(track_uris):
421
+ """
422
+ Creates an organized list of track names.
423
+ Used in final return statements by functions below.
424
+ """
425
+ track_details = sp.tracks(track_uris)
426
+ track_names_with_artists = [f"{track['name']} by {track['artists'][0]['name']}" for track in track_details['tracks']]
427
+ track_list_str = "<br>".join(track_names_with_artists)
428
+ return track_list_str
429
+
430
+
431
+ def play_genre_by_name_and_mood(genre_name, user_mood):
432
+ """
433
+ 1. Retrieves user's desired genre and current mood.
434
+ 2. Matches genre and mood to existing options.
435
+ 3. Gets 4 of user's top artists that align with genre.
436
+ 4. Conducts personalized recommendations() search.
437
+ 5. Plays selected track, clears the queue, and adds the rest to the now-empty queue.
438
+ """
439
+ genre_name = get_genre_by_name(genre_name)
440
+ user_mood = get_user_mood(user_mood).lower()
441
+ debug_print(genre_name)
442
+ debug_print(user_mood)
443
+
444
+ # increased personalization
445
+ user_top_artists = sp.current_user_top_artists(limit=NUM_ARTISTS, time_range=TIME_RANGE)
446
+ matching_artists_ids = []
447
+
448
+ for artist in user_top_artists['items']:
449
+ debug_print(artist['genres'])
450
+ for artist_genre in artist['genres']:
451
+ if is_genre_match(genre_name, artist_genre):
452
+ matching_artists_ids.append(artist['id'])
453
+ break # don't waste time cycling artist genres after match
454
+ if len(matching_artists_ids) == MAX_ARTISTS:
455
+ break
456
+
457
+ if not matching_artists_ids:
458
+ matching_artists_ids = None
459
+ else:
460
+ artist_names = [artist['name'] for artist in sp.artists(matching_artists_ids)['artists']]
461
+ debug_print(artist_names)
462
+ debug_print(matching_artists_ids)
463
+
464
+ recommendations = sp.recommendations( # accepts maximum {genre + artists} = 5 seeds
465
+ seed_artists=matching_artists_ids,
466
+ seed_genres=[genre_name],
467
+ seed_tracks=None,
468
+ limit=NUM_TRACKS, # number of tracks to return
469
+ country=None,
470
+ **MOOD_SETTINGS[user_mood]) # maps to mood settings dictionary
471
+
472
+ track_uris = [track['uri'] for track in recommendations['tracks']]
473
+ track_list_str = create_track_list_str(track_uris)
474
+ sp.start_playback(device_id=device_id.value, uris=track_uris)
475
+
476
+ return f"""
477
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{genre_name}</span> ♫
478
+
479
+ **Selected Tracks:**
480
+ {track_list_str}
481
+ """
482
+
483
+
484
+ ### ### ### Artist + Mood ### ### ###
485
+
486
+
487
+ def play_artist_by_name_and_mood(artist_name, user_mood):
488
+ """
489
+ Plays tracks (randomly selected) by a given artist that matches the user's mood.
490
+ """
491
+ user_mood = get_user_mood(user_mood).lower()
492
+ debug_print(user_mood)
493
+
494
+ # retrieving and shuffling all artist's tracks
495
+ first_name = artist_name.split(',')[0].strip()
496
+ results = sp.search(q=first_name, type='artist')
497
+ artist_id = results['artists']['items'][0]['id']
498
+ # most recent albums retrieved first
499
+ artist_albums = sp.artist_albums(artist_id, album_type='album', limit=NUM_ALBUMS)
500
+ artist_tracks = []
501
+ for album in artist_albums['items']:
502
+ album_tracks = sp.album_tracks(album['id'])['items']
503
+ artist_tracks.extend(album_tracks)
504
+ random.shuffle(artist_tracks)
505
+
506
+ # filtering until we find enough (MAX_TRACKS) tracks that match user's mood
507
+ selected_tracks = []
508
+ for track in artist_tracks:
509
+ if len(selected_tracks) == MAX_TRACKS:
510
+ break
511
+ features = sp.audio_features([track['id']])[0]
512
+ mood_criteria = MOOD_SETTINGS[user_mood]
513
+
514
+ match = True
515
+ for criteria, threshold in mood_criteria.items():
516
+ if "min_" in criteria and features[criteria[4:]] < threshold:
517
+ match = False
518
+ break
519
+ elif "max_" in criteria and features[criteria[4:]] > threshold:
520
+ match = False
521
+ break
522
+ if match:
523
+ debug_print(f"{features}\n{mood_criteria}\n\n")
524
+ selected_tracks.append(track)
525
+
526
+ track_names = [track['name'] for track in selected_tracks]
527
+ track_list_str = "<br>".join(track_names) # using HTML line breaks for each track name
528
+ debug_print(track_list_str)
529
+ track_uris = [track['uri'] for track in selected_tracks]
530
+ sp.start_playback(device_id=device_id.value, uris=track_uris)
531
+
532
+ return f"""
533
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{artist_name}</span> ♫
534
+
535
+ **Selected Tracks:**
536
+ {track_list_str}
537
+ """
538
+
539
+
540
+ ### ### ### Recommendations ### ### ###
541
+
542
+
543
+ def recommend_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
544
+ """
545
+ 1. Retrieves user's preferences based on artist_name, track_name, genre_name, and/or user_mood.
546
+ 2. Uses these parameters to conduct personalized recommendations() search.
547
+ 3. Returns the track URIs of (NUM_TRACKS) recommendation tracks.
548
+ """
549
+ user_mood = get_user_mood(user_mood).lower() if user_mood else None
550
+ debug_print(user_mood)
551
+
552
+ seed_genre, seed_artist, seed_track = None, None, None
553
+
554
+ if genre_name:
555
+ first_name = genre_name.split(',')[0].strip()
556
+ genre_name = get_genre_by_name(first_name)
557
+ seed_genre = [genre_name]
558
+ debug_print(seed_genre)
559
+
560
+ if artist_name:
561
+ first_name = artist_name.split(',')[0].strip() # if user provides multiple artists, use the first
562
+ results = sp.search(q='artist:' + first_name, type='artist')
563
+ seed_artist = [results['artists']['items'][0]['id']]
564
+
565
+ if track_name:
566
+ first_name = track_name.split(',')[0].strip()
567
+ results = sp.search(q='track:' + first_name, type='track')
568
+ seed_track = [results['tracks']['items'][0]['id']]
569
+
570
+ # if user requests recommendations without specifying anything but their mood
571
+ # this is because recommendations() requires at least one seed
572
+ if seed_genre is None and seed_artist is None and seed_track is None:
573
+ raise ValueError("At least one genre, artist, or track must be provided.")
574
+
575
+ recommendations = sp.recommendations( # passing in 3 seeds
576
+ seed_artists=seed_artist,
577
+ seed_genres=seed_genre,
578
+ seed_tracks=seed_track,
579
+ limit=NUM_TRACKS,
580
+ country=None,
581
+ **MOOD_SETTINGS[user_mood] if user_mood else {})
582
+
583
+ track_uris = [track['uri'] for track in recommendations['tracks']]
584
+ return track_uris
585
+
586
+
587
+ def play_recommended_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
588
+ """
589
+ Plays the track_uris returned by recommend_tracks().
590
+ """
591
+ try:
592
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
593
+ track_list_str = create_track_list_str(track_uris)
594
+ sp.start_playback(device_id=device_id.value, uris=track_uris)
595
+
596
+ return f"""
597
+ **♫ Now Playing Recommendations Based On:** <span style="color: red; font-weight: bold; font-style: italic;">
598
+ {', '.join(filter(None, [genre_name, artist_name, track_name, "Your Mood"]))}</span> ♫
599
+
600
+ **Selected Tracks:**
601
+ {track_list_str}
602
+ """
603
+ except ValueError as e:
604
+ return str(e)
605
+
606
+
607
+ def create_playlist_from_recommendations(genre_name=None, artist_name=None, track_name=None, user_mood=None):
608
+ """
609
+ Creates a playlist from recommend_tracks().
610
+ """
611
+ user = sp.current_user()
612
+ user_id = user['id']
613
+ user_name = user["display_name"]
614
+
615
+ playlists = sp.current_user_playlists()
616
+ playlist_names = [playlist['name'] for playlist in playlists["items"]]
617
+ chosen_theme = random.choice(THEMES)
618
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
619
+ # ensuring the use of new adjective each time
620
+ while playlist_name in playlist_names:
621
+ chosen_theme = random.choice(THEMES)
622
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
623
+
624
+ playlist_description=f"Apollo AI's personalized playlist for {user_name}. Get yours here: (add link)." # TODO: add link to project
625
+ new_playlist = sp.user_playlist_create(user=user_id, name=playlist_name,
626
+ public=True, collaborative=False, description=playlist_description)
627
+
628
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
629
+ track_list_str = create_track_list_str(track_uris)
630
+ sp.user_playlist_add_tracks(user=user_id, playlist_id=new_playlist['id'], tracks=track_uris, position=None)
631
+ playlist_url = f"https://open.spotify.com/playlist/{new_playlist['id']}"
632
+
633
+ return f"""
634
+ ♫ Created *{playlist_name}* Based On: <span style='color: red; font-weight: bold; font-style: italic;'>
635
+ {', '.join(filter(None, [genre_name, artist_name, track_name, 'Your Mood']))}</span> ♫
636
+
637
+ **Selected Tracks:**
638
+ {track_list_str}
639
+
640
+ <a href='{playlist_url}'>Click here to listen to the playlist on Spotify!</a>
641
+ """
642
+
643
+
644
+ # ------------------------------
645
+ # Section: Agent Tools
646
+ # ------------------------------
647
+
648
+
649
+ class TrackNameInput(BaseModel):
650
+ track_name: str = Field(description="Track name in the user's request.")
651
+
652
+
653
+ class AlbumNameAndArtistNameInput(BaseModel):
654
+ album_name: str = Field(description="Album name in the user's request.")
655
+ artist_name: str = Field(description="Artist name in the user's request.")
656
+
657
+
658
+ class PlaylistNameInput(BaseModel):
659
+ playlist_name: str = Field(description="Playlist name in the user's request.")
660
+
661
+
662
+ class GenreNameAndUserMoodInput(BaseModel):
663
+ genre_name: str = Field(description="Genre name in the user's request.")
664
+ user_mood: str = Field(description="User's current mood/state-of-being.")
665
+
666
+
667
+ class ArtistNameAndUserMoodInput(BaseModel):
668
+ artist_name: str = Field(description="Artist name in the user's request.")
669
+ user_mood: str = Field(description="User's current mood/state-of-being.")
670
+
671
+
672
+ class RecommendationsInput(BaseModel):
673
+ genre_name: str = Field(description="Genre name in the user's request.")
674
+ artist_name: str = Field(description="Artist name in the user's request.")
675
+ track_name: str = Field(description="Track name in the user's request.")
676
+ user_mood: str = Field(description="User's current mood/state-of-being.")
677
+
678
+
679
+ @tool("play_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
680
+ def tool_play_track_by_name(track_name: str) -> str:
681
+ """
682
+ Use this tool when a user wants to play a particular track by its name.
683
+ You will need to identify the track name from the user's request.
684
+ Usually, the requests will look like 'play {track name}'.
685
+ This tool is specifically designed for clear and accurate track requests.
686
+ """
687
+ return play_track_by_name(track_name)
688
+
689
+
690
+ @tool("queue_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
691
+ def tool_queue_track_by_name(track_name: str) -> str:
692
+ """
693
+ Always use this tool when a user says "queue" in their request.
694
+ """
695
+ return queue_track_by_name(track_name)
696
+
697
+
698
+ @tool("pause_track", return_direct=RETURN_DIRECT)
699
+ def tool_pause_track(query: str) -> str:
700
+ """
701
+ Always use this tool when a user says "pause" or "stop" in their request.
702
+ """
703
+ return pause_track()
704
+
705
+
706
+ @tool("resume_track", return_direct=RETURN_DIRECT)
707
+ def tool_resume_track(query: str) -> str:
708
+ """
709
+ Always use this tool when a user says "resume" or "unpause" in their request.
710
+ """
711
+ return resume_track()
712
+
713
+
714
+ @tool("skip_track", return_direct=RETURN_DIRECT)
715
+ def tool_skip_track(query: str) -> str:
716
+ """
717
+ Always use this tool when a user says "skip" or "next" in their request.
718
+ """
719
+ return skip_track()
720
+
721
+
722
+ @tool("play_album_by_name_and_artist", return_direct=RETURN_DIRECT, args_schema=AlbumNameAndArtistNameInput)
723
+ def tool_play_album_by_name_and_artist(album_name: str, artist_name: str) -> str:
724
+ """
725
+ Use this tool when a user wants to play an album.
726
+ You will need to identify both the album name and artist name from the user's request.
727
+ Usually, the requests will look like 'play the album {album_name} by {artist_name}'.
728
+ """
729
+ return play_album_by_name_and_artist(album_name, artist_name)
730
+
731
+
732
+ @tool("play_playlist_by_name", return_direct=RETURN_DIRECT, args_schema=PlaylistNameInput)
733
+ def tool_play_playlist_by_name(playlist_name: str) -> str:
734
+ """
735
+ Use this tool when a user wants to play one of their playlists.
736
+ You will need to identify the playlist name from the user's request.
737
+ """
738
+ return play_playlist_by_name(playlist_name)
739
+
740
+
741
+ @tool("explain_track", return_direct=RETURN_DIRECT)
742
+ def tool_explain_track(query: str) -> str:
743
+ """
744
+ Use this tool when a user wants to know about the currently playing track.
745
+ """
746
+ return explain_track()
747
+
748
+
749
+ @tool("play_genre_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=GenreNameAndUserMoodInput)
750
+ def tool_play_genre_by_name_and_mood(genre_name: str, user_mood: str) -> str:
751
+ """
752
+ Use this tool when a user wants to play a genre.
753
+ You will need to identify both the genre name from the user's request,
754
+ and also their current mood, which you should always be monitoring.
755
+ """
756
+ return play_genre_by_name_and_mood(genre_name, user_mood)
757
+
758
+
759
+ @tool("play_artist_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=ArtistNameAndUserMoodInput)
760
+ def tool_play_artist_by_name_and_mood(artist_name: str, user_mood: str) -> str:
761
+ """
762
+ Use this tool when a user wants to play an artist.
763
+ You will need to identify both the artist name from the user's request,
764
+ and also their current mood, which you should always be monitoring.
765
+ If you don't know the user's mood, ask them before using this tool.
766
+ """
767
+ return play_artist_by_name_and_mood(artist_name, user_mood)
768
+
769
+
770
+ @tool("play_recommended_tracks", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
771
+ def tool_play_recommended_tracks(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
772
+ """
773
+ Use this tool when a user wants track recommendations.
774
+ You will need to identify the genre name, artist name, and/or track name
775
+ from the user's request... and also their current mood, which you should always be monitoring.
776
+ The user must provide at least genre, artist, or track.
777
+ """
778
+ return play_recommended_tracks(genre_name, artist_name, track_name, user_mood)
779
+
780
+
781
+ @tool("create_playlist_from_recommendations", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
782
+ def tool_create_playlist_from_recommendations(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
783
+ """
784
+ Use this tool when a user wants a playlist created (from recommended tracks).
785
+ You will need to identify the genre name, artist name, and/or track name
786
+ from the user's request... and also their current mood, which you should always be monitoring.
787
+ The user must provide at least genre, artist, or track.
788
+ """
789
+ return create_playlist_from_recommendations(genre_name, artist_name, track_name, user_mood)
790
+
791
+
792
+ CUSTOM_TOOLS =[
793
+ tool_play_track_by_name,
794
+ tool_queue_track_by_name,
795
+ tool_pause_track,
796
+ tool_resume_track,
797
+ tool_skip_track,
798
+ tool_play_album_by_name_and_artist,
799
+ tool_play_playlist_by_name,
800
+ tool_explain_track,
801
+ tool_play_genre_by_name_and_mood,
802
+ tool_play_artist_by_name_and_mood,
803
+ tool_play_recommended_tracks,
804
+ tool_create_playlist_from_recommendations
805
+ ]
806
+
807
+
808
+ # ------------------------------
809
+ # Section: Chatbot
810
+ # ------------------------------
811
+
812
+
813
+ system_message = SystemMessage(content=SYSTEM_MESSAGE)
814
+ MEMORY_KEY = "chat_history"
815
+ prompt = OpenAIFunctionsAgent.create_prompt(
816
+ system_message=system_message,
817
+ extra_prompt_messages=[MessagesPlaceholder(variable_name=MEMORY_KEY)]
818
+ )
819
+ memory = ConversationBufferMemory(memory_key=MEMORY_KEY, return_messages=True)
820
+ llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, max_retries=3, temperature=0, model=MODEL)
821
+ agent = OpenAIFunctionsAgent(llm=llm, tools=CUSTOM_TOOLS, prompt=prompt)
822
+ agent_executor = AgentExecutor(agent=agent, tools=CUSTOM_TOOLS, memory=memory, verbose=True)
823
+
824
+ return agent_executor
825
+
826
+ create_agent_button.click(create_agent, inputs=[ACCESS_TOKEN_VAR], outputs=[AGENT_EXECUTOR_VAR])
827
+
828
+
829
+ # ------------------------------
830
+ # Section: Chat Interface
831
+ # ------------------------------
832
+
833
+ chatbot = gr.Chatbot()
834
+ msg = gr.Textbox()
835
+
836
+ def respond(user_message, chat_history, agent_executor):
837
+ bot_message = agent_executor.run(user_message)
838
+ chat_history.append((user_message, bot_message))
839
+ time.sleep(2)
840
+ return "", chat_history
841
+
842
+ msg.submit(respond, inputs=[msg, chatbot, AGENT_EXECUTOR_VAR], outputs=[msg, chatbot])
843
+
844
+ auth_page.launch(share=True)
messages.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_MESSAGE = """
2
+ You are Apollo, an AI music-player assistant, designed to provide a personalized and engaging listening experience through thoughtful interaction and intelligent tool usage.
3
+
4
+ Your Main Responsibilities:
5
+
6
+ 1. **Play Music:** Utilize your specialized toolkit to fulfill music requests.
7
+
8
+ 2. **Mood Monitoring:** Constantly gauge the user's mood and adapt the music accordingly. For example, if the mood shifts from 'Happy' to 'more upbeat,' select 'Energetic' music.
9
+
10
+ 3. **Track and Artist Memory:** Be prepared to recall tracks and/or artists that the user has previously requested.
11
+
12
+ 4. **Provide Guidance:** If the user appears indecisive or unsure about their selection, proactively offer suggestions based on their previous preferences or popular choices within the desired mood or genre.
13
+
14
+ 5. **Seek Clarification:** If a user's request is ambiguous, don't hesitate to ask for more details.
15
+ """
16
+
17
+ GENRE_LIST = [
18
+ 'acoustic', 'afrobeat', 'alt-rock', 'alternative', 'ambient', 'anime',
19
+ 'black-metal', 'bluegrass', 'blues', 'bossanova', 'brazil', 'breakbeat',
20
+ 'british', 'cantopop', 'chicago-house', 'children', 'chill', 'classical',
21
+ 'club', 'comedy', 'country', 'dance', 'dancehall', 'death-metal',
22
+ 'deep-house', 'detroit-techno', 'disco', 'disney', 'drum-and-bass', 'dub',
23
+ 'dubstep', 'edm', 'electro', 'electronic', 'emo', 'folk', 'forro', 'french',
24
+ 'funk', 'garage', 'german', 'gospel', 'goth', 'grindcore', 'groove',
25
+ 'grunge', 'guitar', 'happy', 'hard-rock', 'hardcore', 'hardstyle',
26
+ 'heavy-metal', 'hip-hop', 'holidays', 'honky-tonk', 'house', 'idm',
27
+ 'indian', 'indie', 'indie-pop', 'industrial', 'iranian', 'j-dance',
28
+ 'j-idol', 'j-pop', 'j-rock', 'jazz', 'k-pop', 'kids', 'latin', 'latino',
29
+ 'malay', 'mandopop', 'metal', 'metal-misc', 'metalcore', 'minimal-techno',
30
+ 'movies', 'mpb', 'new-age', 'new-release', 'opera', 'pagode', 'party',
31
+ 'philippines-opm', 'piano', 'pop', 'pop-film', 'post-dubstep', 'power-pop',
32
+ 'progressive-house', 'psych-rock', 'punk', 'punk-rock', 'r-n-b',
33
+ 'rainy-day', 'reggae', 'reggaeton', 'road-trip', 'rock', 'rock-n-roll',
34
+ 'rockabilly', 'romance', 'sad', 'salsa', 'samba', 'sertanejo', 'show-tunes',
35
+ 'singer-songwriter', 'ska', 'sleep', 'songwriter', 'soul', 'soundtracks',
36
+ 'spanish', 'study', 'summer', 'swedish', 'synth-pop', 'tango', 'techno',
37
+ 'trance', 'trip-hop', 'turkish', 'work-out', 'world-music'
38
+ ]
39
+
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==3.40.1
2
+ spotipy==2.23.0
3
+ requests==2.31.0
4
+ beautifulsoup4==4.12.2
5
+ sentence_transformers==2.2.2
6
+ fuzzywuzzy==0.18.0
7
+ numpy==1.25.1
8
+ scikit-learn==1.3.0
9
+ lyricsgenius==3.0.1
10
+ langchain==0.0.271
11
+ pydantic==1.10.11
12
+ openai==0.27.9
13
+ python-dotenv==1.0.0
14
+ huggingface_hub==0.16.4