Spaces:
Paused
Paused
Upload 7 files
Browse files- final_agent.py +53 -0
- final_app.py +81 -0
- final_feedback.py +88 -0
- final_funcs.py +708 -0
- final_msgs.py +148 -0
- final_tools.py +165 -0
- 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 |
+
• Put anything for name and description <br>
|
| 100 |
+
• 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 |
+
stuart.j.waller@vanderbilt.edu
|
| 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 stuart.j.waller@vanderbilt.edu <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
|