sjw commited on
Commit
2dce015
·
1 Parent(s): 0517481

Upload 7 files

Browse files
Files changed (7) hide show
  1. final_agent.py +53 -0
  2. final_app.py +81 -0
  3. final_feedback.py +88 -0
  4. final_funcs.py +708 -0
  5. final_msgs.py +148 -0
  6. final_tools.py +165 -0
  7. requirements.txt +14 -0
final_agent.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from langchain.agents import OpenAIFunctionsAgent, AgentExecutor
3
+ from langchain.chat_models import ChatOpenAI
4
+ from langchain.memory import ConversationBufferMemory
5
+ from langchain.prompts import MessagesPlaceholder
6
+ from langchain.schema import SystemMessage
7
+
8
+ from final_tools import custom_tools
9
+
10
+
11
+ define_agent = """
12
+ You are Apollo, an AI music-player assistant, designed to provide a personalized and engaging listening experience through thoughtful interaction and intelligent tool usage.
13
+
14
+ Your Main Responsibilities:
15
+
16
+ 1. **Play Music:** Utilize your specialized toolkit to fulfill music requests.
17
+
18
+ 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.
19
+
20
+ 3. **Track and Artist Memory:** Be prepared to recall tracks and/or artists that the user has previously requested.
21
+
22
+ 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.
23
+
24
+ 5. **Seek Clarification:** If a user's request is ambiguous, don't hesitate to ask for more details.
25
+ """
26
+
27
+
28
+ # global variable so explain_track() (and future functions that need an llm) can recognize it
29
+ LLM_STATE = gr.State()
30
+ AGENT_EXECUTOR_STATE = gr.State()
31
+
32
+ #MODEL = "gpt-4"
33
+ MODEL = "gpt-3.5-turbo-0613" # best budget option rn
34
+
35
+
36
+ def create_agent(key): # accepts openai_api_key
37
+
38
+ system_message = SystemMessage(content=define_agent)
39
+ MEMORY_KEY = "chat_history"
40
+ prompt = OpenAIFunctionsAgent.create_prompt(
41
+ system_message=system_message,
42
+ extra_prompt_messages=[MessagesPlaceholder(variable_name=MEMORY_KEY)]
43
+ )
44
+ memory = ConversationBufferMemory(memory_key=MEMORY_KEY, return_messages=True)
45
+
46
+ llm = ChatOpenAI(openai_api_key=key, max_retries=3, temperature=0, model=MODEL)
47
+ LLM_STATE.value = llm
48
+
49
+ agent = OpenAIFunctionsAgent(llm=LLM_STATE.value, tools=custom_tools, prompt=prompt)
50
+ agent_executor = AgentExecutor(agent=agent, tools=custom_tools, memory=memory, verbose=True)
51
+ AGENT_EXECUTOR_STATE.value = agent_executor
52
+ return AGENT_EXECUTOR_STATE.value
53
+
final_app.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Standard library imports
2
+ import os
3
+ import time
4
+
5
+ # Related third party imports
6
+ import gradio as gr
7
+ from dotenv import load_dotenv
8
+
9
+ # Local application/library specific imports
10
+ from final_funcs import auth_page, SP_STATE
11
+ from final_agent import create_agent
12
+ from final_feedback import feedback_page
13
+ from final_msgs import INSTRUCTIONS, GREETING, CHAT_HEADER, WARNING
14
+
15
+ load_dotenv()
16
+ KEY = os.getenv("OPENAI_API_KEY")
17
+
18
+
19
+ def add_text(history, text):
20
+ history = history + [(text, None)]
21
+ return history, gr.update(value="", interactive=False)
22
+
23
+
24
+ def bot(history):
25
+ user_input = history[-1][0]
26
+
27
+ if len(history) == 1: # this is the first message from the user
28
+ response = GREETING
29
+
30
+ elif SP_STATE.value is None:
31
+ response = WARNING
32
+
33
+ elif user_input.strip() == '!help': # TODO: streaming !help message looks bad
34
+ response = INSTRUCTIONS
35
+
36
+ else:
37
+ response = agent_executor(user_input, include_run_info=True)
38
+ response = response["output"]
39
+
40
+ history[-1][1] = ""
41
+ for character in response:
42
+ history[-1][1] += character
43
+ gr.update(interactive=True)
44
+ time.sleep(0.0075)
45
+ yield history
46
+
47
+
48
+ with gr.Blocks() as chat_page:
49
+ gr.Markdown(CHAT_HEADER)
50
+ agent_executor = create_agent(KEY)
51
+
52
+ chatbot = gr.Chatbot([], elem_id="chatbot", height=400, label="Apollo 🎵")
53
+
54
+ with gr.Row():
55
+ txt = gr.Textbox(
56
+ show_label=False,
57
+ placeholder="What would you like to hear?",
58
+ container=False
59
+ )
60
+
61
+ txt_msg = txt.submit(add_text, [chatbot, txt], [chatbot, txt], queue=False).then(
62
+ bot, chatbot, chatbot
63
+ )
64
+ txt_msg.then(lambda: gr.update(interactive=True), None, [txt], queue=False)
65
+
66
+ gr.Examples(["Play CRAZY by AXL",
67
+ "I'm feeling great today, match my vibe",
68
+ "Make me a relaxing playlist of SZA-like songs"],
69
+ inputs=[txt], label="")
70
+
71
+ with gr.Accordion(label="Commands & Examples 📜", open=False):
72
+ gr.Markdown(INSTRUCTIONS)
73
+
74
+
75
+ apollo = gr.TabbedInterface([chat_page, auth_page, feedback_page],
76
+ ["Music", "Authentication", "Feedback"],
77
+ theme = "finlaymacklon/boxy_violet")
78
+
79
+ apollo.queue()
80
+ apollo.launch()
81
+
final_feedback.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import csv
3
+ import gradio as gr
4
+ from gradio import components
5
+ from huggingface_hub import Repository, hf_hub_download
6
+ from datetime import datetime
7
+ from final_msgs import FEED_HEADER
8
+
9
+
10
+ DATASET_REPO_URL = "https://huggingface.co/datasets/sjw/data.csv"
11
+ DATASET_REPO_ID = "sjw/data.csv"
12
+ DATA_FILENAME = "data.csv"
13
+ DIRNAME = "data"
14
+ DATA_FILE = os.path.join(DIRNAME, DATA_FILENAME)
15
+ HF_TOKEN = os.environ.get("HF_TOKEN")
16
+
17
+
18
+ # overriding/appending to the gradio template
19
+ SCRIPT = """
20
+ <script>
21
+ if (!window.hasBeenRun) {
22
+ window.hasBeenRun = true;
23
+ console.log("should only happen once");
24
+ document.querySelector("button.submit").click();
25
+ }
26
+ </script>
27
+ """
28
+
29
+
30
+ try:
31
+ hf_hub_download(
32
+ repo_id=DATASET_REPO_ID,
33
+ filename=DATA_FILENAME,
34
+ )
35
+ except:
36
+ print("file not found")
37
+
38
+
39
+ repo = Repository(
40
+ local_dir=DIRNAME,
41
+ clone_from=DATASET_REPO_URL,
42
+ use_auth_token=HF_TOKEN
43
+ )
44
+
45
+
46
+ def generate_html() -> str:
47
+ """Generate HTML content for the chat."""
48
+ with open(DATA_FILE) as csvfile:
49
+ reader = csv.DictReader(csvfile)
50
+ rows = []
51
+ for row in reader:
52
+ rows.append(row)
53
+ rows.reverse()
54
+ if len(rows) == 0:
55
+ return "no messages yet"
56
+ else:
57
+ html = "<div class='chatbot'>"
58
+ for row in rows:
59
+ html += "<div>"
60
+ html += f"<span><b>{row['name']}</b></span> " # Make the name bold and add a space after it
61
+ html += f"<span class='message'>{row['message']}</span>"
62
+ html += "</div>"
63
+ html += "</div>"
64
+ return html
65
+
66
+
67
+ def store_message(name: str, message: str):
68
+ """Store the message and regenerate HTML content."""
69
+ if name and message:
70
+ with open(DATA_FILE, "a") as csvfile:
71
+ writer = csv.DictWriter(csvfile, fieldnames=["name", "message", "time"])
72
+ writer.writerow(
73
+ {"name": name, "message": message, "time": str(datetime.now())}
74
+ )
75
+ commit_url = repo.push_to_hub()
76
+ return generate_html()
77
+
78
+
79
+ with gr.Blocks() as feedback_page:
80
+ gr.Markdown(FEED_HEADER)
81
+ #gr.Markdown(f"Live Dataset: [{DATASET_REPO_URL}]({DATASET_REPO_URL})")
82
+ name_input = components.Textbox(label="Username")
83
+ message_input = components.Textbox(label="Feedback", lines=2)
84
+ output_html = gr.HTML()
85
+ submit_button = gr.Button("Submit")
86
+ submit_button.click(store_message, inputs=[name_input, message_input], outputs=output_html)
87
+
88
+ #feedback_page.launch()
final_funcs.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Standard Library Imports
2
+ import os
3
+ import random
4
+ import re
5
+ from urllib.parse import urlparse, parse_qs
6
+
7
+ # Third-Party Imports
8
+ import gradio as gr
9
+ import lyricsgenius
10
+ import requests
11
+ import spotipy
12
+
13
+ from bs4 import BeautifulSoup
14
+ from dotenv import load_dotenv
15
+ from fuzzywuzzy import fuzz
16
+ from sentence_transformers import SentenceTransformer
17
+ from sklearn.metrics.pairwise import cosine_similarity
18
+ from spotipy.exceptions import SpotifyException
19
+ from requests.exceptions import Timeout
20
+
21
+ # Local Application/Library Specific Imports
22
+ from langchain.schema import HumanMessage, SystemMessage
23
+ from final_msgs import AUTH_HEADER, DISCLAIMER, LOCAL_INSTALL, NEED_SPOTIFY
24
+
25
+ load_dotenv()
26
+
27
+
28
+ ### ### ### Global Settings ### ### ###
29
+
30
+
31
+ DEBUG_MODE = True # set to False to disable print statements
32
+ def debug_print(*args, **kwargs):
33
+ if DEBUG_MODE:
34
+ print(*args, **kwargs)
35
+
36
+ REDIRECT_URI = "https://huggingface.co/sjw" # TODO: switch to personal website
37
+
38
+ # as required by the functions
39
+ SCOPE = ['user-library-read',
40
+ 'user-read-playback-state',
41
+ 'user-modify-playback-state',
42
+ 'playlist-modify-public',
43
+ 'user-top-read']
44
+
45
+ # for play_genre_by_name_and_mood, play_artist_by_name_and_mood, and recommend_tracks()
46
+ MOOD_SETTINGS = {
47
+ "happy": {"max_instrumentalness": 0.001, "min_valence": 0.6},
48
+ "sad": {"max_danceability": 0.65, "max_valence": 0.4},
49
+ "energetic": {"min_tempo": 120, "min_danceability": 0.75},
50
+ "calm": {"max_energy": 0.65, "max_tempo": 130}
51
+ }
52
+
53
+ # for play_genre_by_name_and_mood
54
+ NUM_ARTISTS = 20 # number of artists to retrieve from user's top artists; function accepts max 50
55
+ TIME_RANGE = "medium_term" # the time frame in which affinities are computed valid-values; short_term, medium_term, long_term
56
+ NUM_TRACKS = 10 # number of tracks to return; also used by recommend_tracks()
57
+ MAX_ARTISTS = 4 # recommendations() accepts a maximum of 5 seeds; in this case, genre will always be 1/5
58
+
59
+ # for play_artist_by_name_and_mood()
60
+ NUM_ALBUMS = 20 # number of albums to retrieve at a maximum; function accepts max 20
61
+ MAX_TRACKS = 10 # number of tracks to randomly select from artist's albums
62
+
63
+
64
+ ### ### ### Other Globals ### ### ###
65
+
66
+
67
+ # NOTE: extremely important; ensures user isolation
68
+ SP_STATE = gr.State()
69
+ DEVICE_ID_STATE = gr.State()
70
+
71
+ # for get_genre_by_name()
72
+ # created states to avoid using global variables when possible
73
+ GENRE_LIST = gr.State()
74
+ GENRE_EMBEDDINGS = gr.State()
75
+
76
+ AUTH_MSG = "Spotify client not initialized. Authenticate Spotify first."
77
+
78
+ # for explain_track()
79
+ GENIUS_TOKEN = os.getenv("GENIUS_ACCESS_TOKEN")
80
+
81
+ # for play_playlist_by_name() and get_user_mood()
82
+ # popular smaller/faster BERT; 6 layers as opposed to 12/24
83
+ MODEL = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
84
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # satisfies warning
85
+
86
+ # for get_user_mood()
87
+ MOOD_LIST = ["happy", "sad", "energetic", "calm"]
88
+ MOOD_EMBEDDINGS = MODEL.encode(MOOD_LIST)
89
+
90
+ # adjectives for playlist names
91
+ THEMES = ["Epic", "Hypnotic", "Dreamy", "Legendary", "Majestic",
92
+ "Enchanting", "Ethereal", "Super Lit", "Harmonious", "Heroic"]
93
+
94
+
95
+ ### ### ### User Authentication ### ### ###
96
+
97
+
98
+ with gr.Blocks() as auth_page:
99
+ gr.Markdown(AUTH_HEADER)
100
+
101
+ with gr.Row():
102
+ client_id = gr.Textbox(placeholder="5. Paste Spotify Client ID here, then click the button below", container=False, text_align="center")
103
+ generate_link = gr.Button("6. Get Authentication Link")
104
+ display_link = gr.Markdown()
105
+
106
+ url = gr.Textbox(placeholder="7. Paste entire URL here, then click the button below", container=False, text_align="center")
107
+ authorize_url = gr.Button("8. Authorize URL")
108
+ auth_result = gr.Markdown()
109
+
110
+ def spotify_auth(client_id, url=None):
111
+ """
112
+ Authenticate Spotify with the provided client_id and url.
113
+ """
114
+ if url:
115
+ parsed_url = urlparse(url)
116
+ fragment = parsed_url.fragment
117
+ access_token = parse_qs(fragment)['access_token'][0]
118
+
119
+ # NOTE: creating distinct Spotify states for each user
120
+ sp = spotipy.Spotify(auth=access_token)
121
+ SP_STATE.value = sp
122
+
123
+ device_id = SP_STATE.value.devices()['devices'][0]['id']
124
+ DEVICE_ID_STATE.value = device_id
125
+
126
+ # TODO: this is overkill; should probably just hardcode the genres
127
+ GENRE_LIST.value = SP_STATE.value.recommendation_genre_seeds()["genres"]
128
+ GENRE_EMBEDDINGS.value = MODEL.encode(GENRE_LIST.value)
129
+
130
+ debug_print(SP_STATE.value)
131
+ debug_print(DEVICE_ID_STATE.value)
132
+
133
+ #return access_token # proof of distinct user sessions
134
+ return """
135
+ <span style="font-size:18px;">Authentication Success.</span>
136
+ """
137
+ else:
138
+ auth_url = (
139
+ f"https://accounts.spotify.com/authorize?response_type=token&client_id={client_id}"
140
+ f"&scope={'%20'.join(SCOPE)}&redirect_uri={REDIRECT_URI}"
141
+ )
142
+ return ("""<span style="font-size:18px;">Authorize by clicking <strong><a href='""" + f"{auth_url}" +
143
+ """' target="_blank">here</a></strong> and copy the '<strong>entire URL</strong>' you are redirected to</span>""")
144
+
145
+
146
+ generate_link.click(spotify_auth, inputs=[client_id], outputs=display_link)
147
+ authorize_url.click(spotify_auth, inputs=[client_id, url], outputs=auth_result)
148
+
149
+ with gr.Accordion(label="Local Installation 💻", open=False):
150
+ gr.Markdown(LOCAL_INSTALL)
151
+
152
+ with gr.Accordion(label="Don't Have Spotify 🫴?", open=False):
153
+ gr.Markdown(NEED_SPOTIFY)
154
+
155
+ with gr.Accordion(label="Security & Privacy 🛡️", open=False):
156
+ gr.Markdown(DISCLAIMER)
157
+
158
+
159
+ ### ### ### Basic Functions ### ### ###
160
+
161
+
162
+ def find_track_by_name(track_name):
163
+ """
164
+ Finds the Spotify track URI given the track name.
165
+ """
166
+ if SP_STATE.value is None:
167
+ return f"{AUTH_MSG}"
168
+
169
+ results = SP_STATE.value.search(q=track_name, type='track')
170
+ track_uri = results['tracks']['items'][0]['uri']
171
+ return track_uri
172
+
173
+
174
+ def play_track_by_name(track_name):
175
+ """
176
+ Plays a track given its name. Uses the above function.
177
+ """
178
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
179
+ return f"{AUTH_MSG}"
180
+
181
+ track_uri = find_track_by_name(track_name)
182
+ track_name = SP_STATE.value.track(track_uri)["name"]
183
+ artist_name = SP_STATE.value.track(track_uri)['artists'][0]['name']
184
+
185
+ try:
186
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, uris=[track_uri])
187
+ return f"♫ Now playing {track_name} by {artist_name} ♫"
188
+ except SpotifyException as e:
189
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
190
+ except Exception as e:
191
+ return f"An unexpected error occurred: {e}."
192
+
193
+
194
+ def queue_track_by_name(track_name):
195
+ """
196
+ Queues track given its name.
197
+ """
198
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
199
+ return f"{AUTH_MSG}"
200
+
201
+ track_uri = find_track_by_name(track_name)
202
+ track_name = SP_STATE.value.track(track_uri)["name"]
203
+ SP_STATE.value.add_to_queue(uri=track_uri, device_id=DEVICE_ID_STATE.value)
204
+ return f"♫ Added {track_name} to your queue ♫"
205
+
206
+
207
+ def pause_track():
208
+ """
209
+ Pauses the current playback.
210
+ """
211
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
212
+ return f"{AUTH_MSG}"
213
+ SP_STATE.value.pause_playback(device_id=DEVICE_ID_STATE.value)
214
+ return "♫ Playback paused ♫"
215
+
216
+
217
+ def resume_track():
218
+ """
219
+ Resumes the current playback.
220
+ """
221
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
222
+ return f"{AUTH_MSG}"
223
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value)
224
+ return "♫ Playback started ♫"
225
+
226
+
227
+ def skip_track():
228
+ """
229
+ Skips the current playback.
230
+ """
231
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
232
+ return f"{AUTH_MSG}"
233
+ SP_STATE.value.next_track(device_id=DEVICE_ID_STATE.value)
234
+ return "♫ Skipped to your next track ♫"
235
+
236
+
237
+ ### ### ### More Elaborate Functions ### ### ###
238
+
239
+
240
+ def play_album_by_name_and_artist(album_name, artist_name):
241
+ """
242
+ Plays an album given its name and the artist.
243
+ context_uri (provide a context_uri to start playback of an album, artist, or playlist) expects a string.
244
+ """
245
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
246
+ return f"{AUTH_MSG}"
247
+
248
+ results = SP_STATE.value.search(q=f'{album_name} {artist_name}', type='album')
249
+ album_id = results['albums']['items'][0]['id']
250
+ album_info = SP_STATE.value.album(album_id)
251
+ album_name = album_info['name']
252
+ artist_name = album_info['artists'][0]['name']
253
+
254
+ try:
255
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, context_uri=f'spotify:album:{album_id}')
256
+ return f"♫ Now playing {album_name} by {artist_name} ♫"
257
+ except spotipy.SpotifyException as e:
258
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
259
+ except Timeout:
260
+ return f"An unexpected error occurred: {e}."
261
+
262
+
263
+ def play_playlist_by_name(playlist_name):
264
+ """
265
+ Plays an existing playlist in the user's library given its name.
266
+ """
267
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
268
+ return f"{AUTH_MSG}"
269
+
270
+ playlists = SP_STATE.value.current_user_playlists()
271
+ playlist_dict = {playlist['name']: (playlist['id'], playlist['owner']['display_name']) for playlist in playlists['items']}
272
+ playlist_names = [key for key in playlist_dict.keys()]
273
+
274
+ # defined inside to capture user-specific playlists
275
+ playlist_name_embeddings = MODEL.encode(playlist_names)
276
+ user_playlist_embedding = MODEL.encode([playlist_name])
277
+
278
+ # compares (embedded) given name to (embedded) playlist library and outputs the closest match
279
+ similarity_scores = cosine_similarity(user_playlist_embedding, playlist_name_embeddings)
280
+ most_similar_index = similarity_scores.argmax()
281
+ playlist_name = playlist_names[most_similar_index]
282
+
283
+ try:
284
+ playlist_id, creator_name = playlist_dict[playlist_name]
285
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, context_uri=f'spotify:playlist:{playlist_id}')
286
+ return f'♫ Now playing {playlist_name} by {creator_name} ♫'
287
+ except:
288
+ return "Unable to find playlist. Please try again."
289
+
290
+
291
+ def get_track_info():
292
+ """
293
+ Harvests information for explain_track() using Genius' API and basic webscraping.
294
+ """
295
+ if SP_STATE.value is None:
296
+ return f"{AUTH_MSG}"
297
+
298
+ current_track_item = SP_STATE.value.current_user_playing_track()['item']
299
+ track_name = current_track_item['name']
300
+ artist_name = current_track_item['artists'][0]['name']
301
+ album_name = current_track_item['album']['name']
302
+ release_date = current_track_item['album']['release_date']
303
+ basic_info = {
304
+ 'track_name': track_name,
305
+ 'artist_name': artist_name,
306
+ 'album_name': album_name,
307
+ 'release_date': release_date,
308
+ }
309
+
310
+ # define inside to avoid user conflicts (simultaneously query Genius)
311
+ genius = lyricsgenius.Genius(GENIUS_TOKEN)
312
+ # removing feature information from song titles to avoid scewing search
313
+ track_name = re.split(' \(with | \(feat\. ', track_name)[0]
314
+ result = genius.search_song(track_name, artist_name)
315
+
316
+ # if no Genius page exists
317
+ if result is not None and hasattr(result, 'artist'):
318
+ genius_artist = result.artist.lower().replace(" ", "")
319
+ spotify_artist = artist_name.lower().replace(" ", "")
320
+ debug_print(spotify_artist)
321
+ debug_print(genius_artist)
322
+ if spotify_artist not in genius_artist:
323
+ return basic_info, None, None, None
324
+ else:
325
+ genius_artist = None
326
+ return basic_info, None, None, None
327
+
328
+ # if Genius page exists
329
+ lyrics = result.lyrics
330
+ url = result.url
331
+ response = requests.get(url)
332
+
333
+ # parsing the webpage and locating 'About' section
334
+ soup = BeautifulSoup(response.text, 'html.parser')
335
+ # universal 'About' section element across all Genius song lyrics pages
336
+ about_section = soup.select_one('div[class^="RichText__Container-oz284w-0"]')
337
+
338
+ # if no 'About' section exists
339
+ if not about_section:
340
+ return basic_info, None, lyrics, url
341
+
342
+ # if 'About' section exists
343
+ else:
344
+ about_section = about_section.get_text(separator='\n')
345
+ return basic_info, about_section, lyrics, url
346
+
347
+
348
+ def explain_track():
349
+ """
350
+ Displays track information in an organized, informational, and compelling manner.
351
+ Uses the above function.
352
+ """
353
+
354
+ # defined inside to avoid circular importing
355
+ from final_agent import LLM_STATE
356
+
357
+ basic_info, about_section, lyrics, url = get_track_info()
358
+ debug_print(basic_info, about_section, lyrics, url)
359
+
360
+ if lyrics: # if Genius page exists
361
+ system_message_content = """
362
+ Your task is to create an engaging summary for a track using the available details
363
+ about the track and its lyrics. If there's insufficient or no additional information
364
+ besides the lyrics, craft the entire summary based solely on the lyrical content."
365
+ """
366
+ human_message_content = f"{about_section}\n\n{lyrics}"
367
+ messages = [
368
+ SystemMessage(content=system_message_content),
369
+ HumanMessage(content=human_message_content)
370
+ ]
371
+ ai_response = LLM_STATE.value(messages).content
372
+ summary = f"""
373
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
374
+ **Artist:** {basic_info["artist_name"]}
375
+ **Album:** {basic_info["album_name"]}
376
+ **Release:** {basic_info["release_date"]}
377
+
378
+ **About:**
379
+ {ai_response}
380
+
381
+ <a href='{url}'>Click here for more information on Genius!</a>
382
+ """
383
+ return summary
384
+
385
+ else: # if no Genius page exists
386
+ url = "https://genius.com/Genius-how-to-add-songs-to-genius-annotated"
387
+ summary = f"""
388
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
389
+ **Artist:** {basic_info["artist_name"]}
390
+ **Album:** {basic_info["album_name"]}
391
+ **Release:** {basic_info["release_date"]}
392
+
393
+ **About:**
394
+ Unfortunately, this track has not been uploaded to Genius.com
395
+
396
+ <a href='{url}'>Be the first to change that!</a>
397
+ """
398
+ return summary
399
+
400
+
401
+ ### ### ### Genre + Mood ### ### ###
402
+
403
+
404
+ def get_user_mood(user_mood):
405
+ """
406
+ Categorizes the user's mood as either 'happy', 'sad', 'energetic', or 'calm'.
407
+ Uses same cosine similarity/embedding concepts as with determining playlist names.
408
+ """
409
+
410
+ if user_mood.lower() in MOOD_LIST:
411
+ user_mood = user_mood.lower()
412
+ return user_mood
413
+ else:
414
+ user_mood_embedding = MODEL.encode([user_mood.lower()])
415
+ similarity_scores = cosine_similarity(user_mood_embedding, MOOD_EMBEDDINGS)
416
+ most_similar_index = similarity_scores.argmax()
417
+ user_mood = MOOD_LIST[most_similar_index]
418
+ return user_mood
419
+
420
+
421
+ def get_genre_by_name(genre_name):
422
+ """
423
+ Matches user's desired genre to closest (most similar) existing genre in the list of genres.
424
+ recommendations() only accepts genres from this list.
425
+ """
426
+
427
+ if genre_name.lower() in GENRE_LIST.value:
428
+ genre_name = genre_name.lower()
429
+ return genre_name
430
+ else:
431
+ genre_name_embedding = MODEL.encode([genre_name.lower()])
432
+ similarity_scores = cosine_similarity(genre_name_embedding, GENRE_EMBEDDINGS.value)
433
+ most_similar_index = similarity_scores.argmax()
434
+ genre_name = GENRE_LIST.value[most_similar_index]
435
+ return genre_name
436
+
437
+
438
+ def is_genre_match(genre1, genre2, threshold=75):
439
+ """
440
+ Determines if two genres are semantically similar.
441
+ token_set_ratio() - for quantifying semantic similarity - and
442
+ threshold of 75 (out of 100) were were arbitrarily determined through basic testing.
443
+ """
444
+
445
+ score = fuzz.token_set_ratio(genre1, genre2)
446
+ debug_print(score)
447
+ return score >= threshold
448
+
449
+
450
+ def create_track_list_str(track_uris):
451
+ """
452
+ Creates an organized list of track names.
453
+ Used in final return statements by functions below.
454
+ """
455
+ if SP_STATE.value is None:
456
+ return f"{AUTH_MSG}"
457
+
458
+ track_details = SP_STATE.value.tracks(track_uris)
459
+ track_names_with_artists = [f"{track['name']} by {track['artists'][0]['name']}" for track in track_details['tracks']]
460
+ track_list_str = "<br>".join(track_names_with_artists)
461
+ return track_list_str
462
+
463
+
464
+ def play_genre_by_name_and_mood(genre_name, user_mood):
465
+ """
466
+ 1. Retrieves user's desired genre and current mood.
467
+ 2. Matches genre and mood to existing options.
468
+ 3. Gets 4 of user's top artists that align with genre.
469
+ 4. Conducts personalized recommendations() search.
470
+ 5. Plays selected track, clears the queue, and adds the rest to the now-empty queue.
471
+ """
472
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
473
+ return f"{AUTH_MSG}"
474
+
475
+ genre_name = get_genre_by_name(genre_name)
476
+ user_mood = get_user_mood(user_mood).lower()
477
+ debug_print(genre_name)
478
+ debug_print(user_mood)
479
+
480
+ # increased personalization
481
+ user_top_artists = SP_STATE.value.current_user_top_artists(limit=NUM_ARTISTS, time_range=TIME_RANGE)
482
+ matching_artists_ids = []
483
+
484
+ for artist in user_top_artists['items']:
485
+ debug_print(artist['genres'])
486
+ for artist_genre in artist['genres']:
487
+ if is_genre_match(genre_name, artist_genre):
488
+ matching_artists_ids.append(artist['id'])
489
+ break # don't waste time cycling artist genres after match
490
+ if len(matching_artists_ids) == MAX_ARTISTS:
491
+ break
492
+
493
+ if not matching_artists_ids:
494
+ matching_artists_ids = None
495
+ else:
496
+ artist_names = [artist['name'] for artist in SP_STATE.value.artists(matching_artists_ids)['artists']]
497
+ debug_print(artist_names)
498
+ debug_print(matching_artists_ids)
499
+
500
+ recommendations = SP_STATE.value.recommendations( # accepts maximum {genre + artists} = 5 seeds
501
+ seed_artists=matching_artists_ids,
502
+ seed_genres=[genre_name],
503
+ seed_tracks=None,
504
+ limit=NUM_TRACKS, # number of tracks to return
505
+ country=None,
506
+ **MOOD_SETTINGS[user_mood]) # maps to mood settings dictionary
507
+
508
+ track_uris = [track['uri'] for track in recommendations['tracks']]
509
+ track_list_str = create_track_list_str(track_uris)
510
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, uris=track_uris)
511
+
512
+ return f"""
513
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{genre_name}</span> ♫
514
+
515
+ **Selected Tracks:**
516
+ {track_list_str}
517
+ """
518
+
519
+
520
+ ### ### ### Artist + Mood ### ### ###
521
+
522
+
523
+ def play_artist_by_name_and_mood(artist_name, user_mood):
524
+ """
525
+ Plays tracks (randomly selected) by a given artist that matches the user's mood.
526
+ """
527
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
528
+ return f"{AUTH_MSG}"
529
+
530
+ user_mood = get_user_mood(user_mood).lower()
531
+ debug_print(user_mood)
532
+
533
+ # retrieving and shuffling all artist's tracks
534
+ first_name = artist_name.split(',')[0].strip()
535
+ results = SP_STATE.value.search(q=first_name, type='artist')
536
+ artist_id = results['artists']['items'][0]['id']
537
+ # most recent albums retrieved first
538
+ artist_albums = SP_STATE.value.artist_albums(artist_id, album_type='album', limit=NUM_ALBUMS)
539
+ artist_tracks = []
540
+ for album in artist_albums['items']:
541
+ album_tracks = SP_STATE.value.album_tracks(album['id'])['items']
542
+ artist_tracks.extend(album_tracks)
543
+ random.shuffle(artist_tracks)
544
+
545
+ # filtering until we find enough (MAX_TRACKS) tracks that match user's mood
546
+ selected_tracks = []
547
+ for track in artist_tracks:
548
+ if len(selected_tracks) == MAX_TRACKS:
549
+ break
550
+ features = SP_STATE.value.audio_features([track['id']])[0]
551
+ mood_criteria = MOOD_SETTINGS[user_mood]
552
+
553
+ match = True
554
+ for criteria, threshold in mood_criteria.items():
555
+ if "min_" in criteria and features[criteria[4:]] < threshold:
556
+ match = False
557
+ break
558
+ elif "max_" in criteria and features[criteria[4:]] > threshold:
559
+ match = False
560
+ break
561
+ if match:
562
+ debug_print(f"{features}\n{mood_criteria}\n\n")
563
+ selected_tracks.append(track)
564
+
565
+ track_names = [track['name'] for track in selected_tracks]
566
+ track_list_str = "<br>".join(track_names) # using HTML line breaks for each track name
567
+ debug_print(track_list_str)
568
+ track_uris = [track['uri'] for track in selected_tracks]
569
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, uris=track_uris)
570
+
571
+ return f"""
572
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{artist_name}</span> ♫
573
+
574
+ **Selected Tracks:**
575
+ {track_list_str}
576
+ """
577
+
578
+
579
+ ### ### ### Recommendations ### ### ###
580
+
581
+
582
+ def recommend_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
583
+ """
584
+ 1. Retrieves user's preferences based on artist_name, track_name, genre_name, and/or user_mood.
585
+ 2. Uses these parameters to conduct personalized recommendations() search.
586
+ 3. Returns the track URIs of (NUM_TRACKS) recommendation tracks.
587
+ """
588
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
589
+ return f"{AUTH_MSG}"
590
+
591
+ user_mood = get_user_mood(user_mood).lower() if user_mood else None
592
+ debug_print(user_mood)
593
+
594
+ seed_genre, seed_artist, seed_track = None, None, None
595
+
596
+ if genre_name:
597
+ first_name = genre_name.split(',')[0].strip()
598
+ genre_name = get_genre_by_name(first_name)
599
+ seed_genre = [genre_name]
600
+ debug_print(seed_genre)
601
+
602
+ if artist_name:
603
+ first_name = artist_name.split(',')[0].strip() # if user provides multiple artists, use the first
604
+ results = SP_STATE.value.search(q='artist:' + first_name, type='artist')
605
+ seed_artist = [results['artists']['items'][0]['id']]
606
+
607
+ if track_name:
608
+ first_name = track_name.split(',')[0].strip()
609
+ results = SP_STATE.value.search(q='track:' + first_name, type='track')
610
+ seed_track = [results['tracks']['items'][0]['id']]
611
+
612
+ # if user requests recommendations without specifying anything but their mood
613
+ # this is because recommendations() requires at least one seed
614
+ if seed_genre is None and seed_artist is None and seed_track is None:
615
+ raise ValueError("At least one genre, artist, or track must be provided.")
616
+
617
+ recommendations = SP_STATE.value.recommendations( # passing in 3 seeds
618
+ seed_artists=seed_artist,
619
+ seed_genres=seed_genre,
620
+ seed_tracks=seed_track,
621
+ limit=NUM_TRACKS,
622
+ country=None,
623
+ **MOOD_SETTINGS[user_mood] if user_mood else {})
624
+
625
+ track_uris = [track['uri'] for track in recommendations['tracks']]
626
+ return track_uris
627
+
628
+
629
+ def play_recommended_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
630
+ """
631
+ Plays the track_uris returned by recommend_tracks().
632
+ """
633
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
634
+ return f"{AUTH_MSG}"
635
+
636
+ try:
637
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
638
+ track_list_str = create_track_list_str(track_uris)
639
+ SP_STATE.value.start_playback(device_id=DEVICE_ID_STATE.value, uris=track_uris)
640
+
641
+ return f"""
642
+ **♫ Now Playing Recommendations Based On:** <span style="color: red; font-weight: bold; font-style: italic;">
643
+ {', '.join(filter(None, [genre_name, artist_name, track_name, "Your Mood"]))}</span> ♫
644
+
645
+ **Selected Tracks:**
646
+ {track_list_str}
647
+ """
648
+ except ValueError as e:
649
+ return str(e)
650
+
651
+
652
+ def create_playlist_from_recommendations(genre_name=None, artist_name=None, track_name=None, user_mood=None):
653
+ """
654
+ Creates a playlist from recommend_tracks().
655
+ """
656
+ if SP_STATE.value is None or DEVICE_ID_STATE.value is None:
657
+ return f"{AUTH_MSG}"
658
+
659
+ user = SP_STATE.value.current_user()
660
+ user_id = user['id']
661
+ user_name = user["display_name"]
662
+
663
+ playlists = SP_STATE.value.current_user_playlists()
664
+ playlist_names = [playlist['name'] for playlist in playlists["items"]]
665
+ chosen_theme = random.choice(THEMES)
666
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
667
+ # ensuring the use of new adjective each time
668
+ while playlist_name in playlist_names:
669
+ chosen_theme = random.choice(THEMES)
670
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
671
+
672
+ playlist_description=f"Apollo AI's personalized playlist for {user_name}. Get yours here: (add link)." # TODO: add link to project
673
+ new_playlist = SP_STATE.value.user_playlist_create(user=user_id, name=playlist_name,
674
+ public=True, collaborative=False, description=playlist_description)
675
+
676
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
677
+ track_list_str = create_track_list_str(track_uris)
678
+ SP_STATE.value.user_playlist_add_tracks(user=user_id, playlist_id=new_playlist['id'], tracks=track_uris, position=None)
679
+ playlist_url = f"https://open.spotify.com/playlist/{new_playlist['id']}"
680
+
681
+ return f"""
682
+ ♫ Created *{playlist_name}* Based On: <span style='color: red; font-weight: bold; font-style: italic;'>
683
+ {', '.join(filter(None, [genre_name, artist_name, track_name, 'Your Mood']))}</span> ♫
684
+
685
+ **Selected Tracks:**
686
+ {track_list_str}
687
+
688
+ <a href='{playlist_url}'>Click here to listen to the playlist on Spotify!</a>
689
+ """
690
+
691
+
692
+
693
+
694
+
695
+
696
+
697
+
698
+
699
+
700
+
701
+
702
+
703
+
704
+
705
+
706
+
707
+
708
+
final_msgs.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+
4
+ ### ### ### Music ### ### ###
5
+
6
+
7
+ INSTRUCTIONS = """
8
+ ### <span style="color: red;"> 💿 Basic 💿</span>
9
+ - **Specific Song:** Play Passionfruit | Play CRAZY by AXL
10
+ - **Controls:** Queue | Pause | Resume | Skip
11
+ - **More Info:** Explain this song
12
+ - **Album:** Play album Utopia | Play album Atlantis by Lilah
13
+ - **Playlist:** Play my Late Night Drive playlist
14
+
15
+ ### <span style="color: red;"> 💿 Personalized 💿</span>
16
+ - **Genre:** I'm happy, play country | Play energetic pop
17
+ - **Artist:** Play Migos hype songs | I'm sad, play Rihanna
18
+ - **Recommendations:** Recommend songs off my love for Drake + r&b
19
+ - **Create Playlist:** Make me a relaxing playlist of SZA-like songs
20
+ """
21
+
22
+ # TODO add an image in the greeting
23
+ GREETING = """
24
+ Hi, I'm Apollo 🤖, here to assist you with **all** your musical desires!
25
+
26
+ ✨ Type **!help** to view commands ✨
27
+
28
+ For a more personalized experience, **tell me your mood**, or what you're doing 🔮
29
+ """
30
+
31
+
32
+ CHAT_HEADER = """
33
+ <div align='center'>
34
+ <h1 style='font-size: 26px;'>🔮 Apollo, Your AI Music Assistant 🔮</h1>
35
+ <br>
36
+ <p style='font-size: 22px;'>Experience personalized, intelligent music interaction</p>
37
+ </div>
38
+
39
+ <div align='center' style='display: flex; align-items: flex-start; justify-content: center;'>
40
+ <div style='flex: 1; text-align: center; padding-right: 10px;'>
41
+ <h2 style='font-size: 20px;'><strong>🎧 Get Started 🎧</strong></h2>
42
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Connect your <span style='color: red;'>Spotify</span></p>
43
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Type <code style='font-size: 24px;'>!help</code> for list of commands</p>
44
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Or see the dropdown below</p>
45
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Feedback is much appreciated</p>
46
+ </div>
47
+ <div>
48
+ <img src='file/mascot1.gif' style='opacity: 1.0;' width='400'/>
49
+ </div>
50
+ <div style='flex: 1; text-align: center; padding-left: 10px;'>
51
+ <h2 style='font-size: 20px;'><strong>🌟 Main Features 🌟</strong></h2>
52
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'><span style='color: red;'>Chat</span> directly with Apollo</p>
53
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Apollo uses your <span style='color: red;'>mood</span> & <span style='color: red;'>preferences</span></p>
54
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Unsure? Apollo <span style='color: red;'>suggests</span></p>
55
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>Mood shift? Apollo <span style='color: red;'>adapts</span></p>
56
+ </div>
57
+ </div>
58
+ <div align='center' style='margin-top: 0 px;'>
59
+ <a href='#' style='font-size: 18px;'>Blog</a> |
60
+ <a href='#' style='font-size: 18px;'>GitHub</a> |
61
+ <a href='#' style='font-size: 18px;'>Website</a> |
62
+ <a href='#' style='font-size: 18px;'>Cool Demo</a> |
63
+ <a href='#' style='font-size: 18px;'>Tutorial</a>
64
+ </div>
65
+ """
66
+
67
+
68
+ WARNING = """
69
+ <div>
70
+ <span style="color: red; font-size: 20px;">I really want to play music for you, but please authenticate your Spotify first</span>
71
+ </div>
72
+ """
73
+
74
+
75
+ # THEME = gr.themes.Soft(primary_hue="red").set(
76
+ # body_background_fill="#F1F4F9",
77
+ # block_border_width="3px",
78
+ # )
79
+
80
+
81
+ ### ### ### Authorization ### ### ###
82
+
83
+
84
+ AUTH_HEADER = """
85
+ <div align='center'>
86
+ <img src='file/mascot2.gif' style='opacity: 1.0;' width='400'/>
87
+ </div>
88
+ <div align='center' style='display: flex; justify-content: space-around; margin-top: 0px;'>
89
+ <div style='text-align: center;'>
90
+ <h2 style='font-size: 20px;'><strong>Local Installation</strong></h2>
91
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>See the dropdown below</p>
92
+ </div>
93
+ <div style='text-align: center;'>
94
+ <h2 style='font-size: 20px;'><strong>🔓 How to Authenticate 🔓</strong></h2>
95
+ <p style='font-size: 18px; line-height: 2.25;'>
96
+ 1. Go to <a href='https://developer.spotify.com/' style='font-size: 18px;'><strong>Spotify Developer</strong></a> <br>
97
+ 2. <strong>'Login'</strong> to your account, then go to <a href='https://developer.spotify.com/dashboard' style='font-size: 18px;'><strong>Dashboard</strong></a> <br>
98
+ 3. <strong>'Create app'</strong> <br>
99
+ &nbsp;&nbsp;• Put anything for name and description <br>
100
+ &nbsp;&nbsp;• For <strong>'Redirect URI'</strong>, put <a href='http://www.jonaswaller.com' style='font-size: 18px;'><strong>www.jonaswaller.com</strong></a> <br>
101
+ 4. Go to your app <strong>'Settings'</strong> and copy the <strong>'Client ID'</strong> <br>
102
+ </p>
103
+ </div>
104
+ <div style='text-align: center;'>
105
+ <h2 style='font-size: 20px;'><strong>Don't have Spotify?</strong></h2>
106
+ <p style='font-size: 18px; text-align: center; line-height: 2.25;'>See the dropdown below</p>
107
+ </div>
108
+ </div>
109
+ """
110
+
111
+
112
+ DISCLAIMER = """
113
+ We utilize Spotify's Implicit Grant Flow which operates entirely client-side,
114
+ eliminating the need for server-side code and storage.
115
+ Apollo has limited permissions - we cannot delete or edit any existing content in your Spotify account;
116
+ access is strictly confined to adding new, curated playlists for you.
117
+ If multiple users are on Apollo simultaneously, each user is confined to their own isolated session,
118
+ preventing any cross-access to your Spotify account.
119
+ Your data is exclusively used for real-time music curation during your active session.
120
+ """
121
+
122
+
123
+ LOCAL_INSTALL = """
124
+ Coming soon
125
+ """
126
+
127
+
128
+ NEED_SPOTIFY = """
129
+ Want to test out Apollo, but don't have Spotify Premium? Email me for a free (testing) account.
130
+
131
132
+ """
133
+
134
+
135
+ ### ### ### Feedback ### ### ###
136
+
137
+
138
+ FEED_HEADER = """
139
+ <div style='text-align: center;'>
140
+ <h2 style='font-size: 20px;'><strong>Thank You!</strong></h2>
141
+ <p style='font-size: 18px; line-height: 2.25;'>
142
+ Your feedback helps improve Apollo <br>
143
+ Or email me [email protected] <br>
144
+ </p>
145
+ </div>
146
+ """
147
+
148
+
final_tools.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from langchain.agents import tool
3
+ import final_funcs as spf
4
+
5
+
6
+ RETURN_DIRECT = True # return output in terminal
7
+
8
+
9
+ class TrackNameInput(BaseModel):
10
+ track_name: str = Field(description="Track name in the user's request.")
11
+
12
+
13
+ class AlbumNameAndArtistNameInput(BaseModel):
14
+ album_name: str = Field(description="Album name in the user's request.")
15
+ artist_name: str = Field(description="Artist name in the user's request.")
16
+
17
+
18
+ class PlaylistNameInput(BaseModel):
19
+ playlist_name: str = Field(description="Playlist name in the user's request.")
20
+
21
+
22
+ class GenreNameAndUserMoodInput(BaseModel):
23
+ genre_name: str = Field(description="Genre name in the user's request.")
24
+ user_mood: str = Field(description="User's current mood/state-of-being.")
25
+
26
+
27
+ class ArtistNameAndUserMoodInput(BaseModel):
28
+ artist_name: str = Field(description="Artist name in the user's request.")
29
+ user_mood: str = Field(description="User's current mood/state-of-being.")
30
+
31
+
32
+ class RecommendationsInput(BaseModel):
33
+ genre_name: str = Field(description="Genre name in the user's request.")
34
+ artist_name: str = Field(description="Artist name in the user's request.")
35
+ track_name: str = Field(description="Track name in the user's request.")
36
+ user_mood: str = Field(description="User's current mood/state-of-being.")
37
+
38
+
39
+ @tool("play_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
40
+ def tool_play_track_by_name(track_name: str) -> str:
41
+ """
42
+ Use this tool when a user wants to play a particular track by its name.
43
+ You will need to identify the track name from the user's request.
44
+ Usually, the requests will look like 'play {track name}'.
45
+ This tool is specifically designed for clear and accurate track requests.
46
+ """
47
+ return spf.play_track_by_name(track_name)
48
+
49
+
50
+ @tool("queue_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
51
+ def tool_queue_track_by_name(track_name: str) -> str:
52
+ """
53
+ Always use this tool when a user says "queue" in their request.
54
+ """
55
+ return spf.queue_track_by_name(track_name)
56
+
57
+
58
+ @tool("pause_track", return_direct=RETURN_DIRECT)
59
+ def tool_pause_track(query: str) -> str:
60
+ """
61
+ Always use this tool when a user says "pause" or "stop" in their request.
62
+ """
63
+ return spf.pause_track()
64
+
65
+
66
+ @tool("resume_track", return_direct=RETURN_DIRECT)
67
+ def tool_resume_track(query: str) -> str:
68
+ """
69
+ Always use this tool when a user says "resume" or "unpause" in their request.
70
+ """
71
+ return spf.resume_track()
72
+
73
+
74
+ @tool("skip_track", return_direct=RETURN_DIRECT)
75
+ def tool_skip_track(query: str) -> str:
76
+ """
77
+ Always use this tool when a user says "skip" or "next" in their request.
78
+ """
79
+ return spf.skip_track()
80
+
81
+
82
+ @tool("play_album_by_name_and_artist", return_direct=RETURN_DIRECT, args_schema=AlbumNameAndArtistNameInput)
83
+ def tool_play_album_by_name_and_artist(album_name: str, artist_name: str) -> str:
84
+ """
85
+ Use this tool when a user wants to play an album.
86
+ You will need to identify both the album name and artist name from the user's request.
87
+ Usually, the requests will look like 'play the album {album_name} by {artist_name}'.
88
+ """
89
+ return spf.play_album_by_name_and_artist(album_name, artist_name)
90
+
91
+
92
+ @tool("play_playlist_by_name", return_direct=RETURN_DIRECT, args_schema=PlaylistNameInput)
93
+ def tool_play_playlist_by_name(playlist_name: str) -> str:
94
+ """
95
+ Use this tool when a user wants to play one of their playlists.
96
+ You will need to identify the playlist name from the user's request.
97
+ """
98
+ return spf.play_playlist_by_name(playlist_name)
99
+
100
+
101
+ @tool("explain_track", return_direct=RETURN_DIRECT)
102
+ def tool_explain_track(query: str) -> str:
103
+ """
104
+ Use this tool when a user wants to know about the currently playing track.
105
+ """
106
+ return spf.explain_track()
107
+
108
+
109
+ @tool("play_genre_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=GenreNameAndUserMoodInput)
110
+ def tool_play_genre_by_name_and_mood(genre_name: str, user_mood: str) -> str:
111
+ """
112
+ Use this tool when a user wants to play a genre.
113
+ You will need to identify both the genre name from the user's request,
114
+ and also their current mood, which you should always be monitoring.
115
+ """
116
+ return spf.play_genre_by_name_and_mood(genre_name, user_mood)
117
+
118
+
119
+ @tool("play_artist_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=ArtistNameAndUserMoodInput)
120
+ def tool_play_artist_by_name_and_mood(artist_name: str, user_mood: str) -> str:
121
+ """
122
+ Use this tool when a user wants to play an artist.
123
+ You will need to identify both the artist name from the user's request,
124
+ and also their current mood, which you should always be monitoring.
125
+ If you don't know the user's mood, ask them before using this tool.
126
+ """
127
+ return spf.play_artist_by_name_and_mood(artist_name, user_mood)
128
+
129
+
130
+ @tool("play_recommended_tracks", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
131
+ def tool_play_recommended_tracks(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
132
+ """
133
+ Use this tool when a user wants track recommendations.
134
+ You will need to identify the genre name, artist name, and/or track name
135
+ from the user's request... and also their current mood, which you should always be monitoring.
136
+ The user must provide at least genre, artist, or track.
137
+ """
138
+ return spf.play_recommended_tracks(genre_name, artist_name, track_name, user_mood)
139
+
140
+
141
+ @tool("create_playlist_from_recommendations", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
142
+ def tool_create_playlist_from_recommendations(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
143
+ """
144
+ Use this tool when a user wants a playlist created (from recommended tracks).
145
+ You will need to identify the genre name, artist name, and/or track name
146
+ from the user's request... and also their current mood, which you should always be monitoring.
147
+ The user must provide at least genre, artist, or track.
148
+ """
149
+ return spf.create_playlist_from_recommendations(genre_name, artist_name, track_name, user_mood)
150
+
151
+
152
+ custom_tools =[
153
+ tool_play_track_by_name,
154
+ tool_queue_track_by_name,
155
+ tool_pause_track,
156
+ tool_resume_track,
157
+ tool_skip_track,
158
+ tool_play_album_by_name_and_artist,
159
+ tool_play_playlist_by_name,
160
+ tool_explain_track,
161
+ tool_play_genre_by_name_and_mood,
162
+ tool_play_artist_by_name_and_mood,
163
+ tool_play_recommended_tracks,
164
+ tool_create_playlist_from_recommendations
165
+ ]
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