Spaces:
Paused
Paused
Delete app.py
Browse files
app.py
DELETED
@@ -1,844 +0,0 @@
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|