Spaces:
Sleeping
Sleeping
Raheel Abdul Rehman
commited on
Commit
·
c31d1ca
1
Parent(s):
1606aeb
Initial Push
Browse files- .dockerignore +0 -0
- .gitignore +181 -0
- Dockerfile +20 -0
- README.md +19 -9
- data/processed/chord_exercises.parquet +3 -0
- data/processed/prod_data.parquet +3 -0
- notebooks/data_visualisation.ipynb +177 -0
- requirements.txt +6 -0
- src/__init__.py +0 -0
- src/data_input.py +27 -0
- src/data_processing.py +317 -0
- src/logger.py +26 -0
- src/main.py +74 -0
- src/model.py +121 -0
- src/spotify_data_fetch.py +98 -0
- src/utils.py +0 -0
- tests/test_utils.py +21 -0
.dockerignore
ADDED
|
File without changes
|
.gitignore
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
|
| 110 |
+
# pdm
|
| 111 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 112 |
+
#pdm.lock
|
| 113 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 114 |
+
# in version control.
|
| 115 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 116 |
+
.pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 121 |
+
__pypackages__/
|
| 122 |
+
|
| 123 |
+
# Celery stuff
|
| 124 |
+
celerybeat-schedule
|
| 125 |
+
celerybeat.pid
|
| 126 |
+
|
| 127 |
+
# SageMath parsed files
|
| 128 |
+
*.sage.py
|
| 129 |
+
|
| 130 |
+
# Environments
|
| 131 |
+
.env
|
| 132 |
+
.venv
|
| 133 |
+
env/
|
| 134 |
+
venv/
|
| 135 |
+
ENV/
|
| 136 |
+
env.bak/
|
| 137 |
+
venv.bak/
|
| 138 |
+
|
| 139 |
+
# Spyder project settings
|
| 140 |
+
.spyderproject
|
| 141 |
+
.spyproject
|
| 142 |
+
|
| 143 |
+
# Rope project settings
|
| 144 |
+
.ropeproject
|
| 145 |
+
|
| 146 |
+
# mkdocs documentation
|
| 147 |
+
/site
|
| 148 |
+
|
| 149 |
+
# mypy
|
| 150 |
+
.mypy_cache/
|
| 151 |
+
.dmypy.json
|
| 152 |
+
dmypy.json
|
| 153 |
+
|
| 154 |
+
# Pyre type checker
|
| 155 |
+
.pyre/
|
| 156 |
+
|
| 157 |
+
# pytype static type analyzer
|
| 158 |
+
.pytype/
|
| 159 |
+
|
| 160 |
+
# Cython debug symbols
|
| 161 |
+
cython_debug/
|
| 162 |
+
|
| 163 |
+
# PyCharm
|
| 164 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 165 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 166 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 167 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 168 |
+
#.idea/
|
| 169 |
+
|
| 170 |
+
# Ruff stuff:
|
| 171 |
+
.ruff_cache/
|
| 172 |
+
|
| 173 |
+
# PyPI configuration file
|
| 174 |
+
.pypirc
|
| 175 |
+
|
| 176 |
+
# Cursor
|
| 177 |
+
# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to
|
| 178 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 179 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 180 |
+
.cursorignore
|
| 181 |
+
.cursorindexingignore
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY --chown=user requirements.txt .
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY --chown=user ./src ./src
|
| 13 |
+
COPY --chown=user ./data/processed ./data/processed
|
| 14 |
+
|
| 15 |
+
RUN mkdir -p /app/logs
|
| 16 |
+
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 20 |
+
|
README.md
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎸 Marks : AI-Powered Guitar Exercise Recommender
|
| 2 |
+
|
| 3 |
+
An AI-driven music recommendation system that suggests guitar exercises and practice routines based on user skill level, tempo preferences, and chord progression complexity.
|
| 4 |
+
This project leverages **machine learning, FastAPI, and Docker** to provide a scalable production-ready API for real-time recommendations.
|
| 5 |
+
|
| 6 |
---
|
| 7 |
+
|
| 8 |
+
## 🚀 Features
|
| 9 |
+
- **Intelligent Recommendations** – Suggests exercises based on chord progressions, difficulty, and tempo.
|
| 10 |
+
- **Clustering & ML Models** – Groups exercises dynamically by difficulty using machine learning techniques.
|
| 11 |
+
- **REST API with FastAPI** – Lightweight, high-performance backend for serving recommendations.
|
| 12 |
+
- **Scalable & Containerized** – Fully Dockerized for easy deployment across cloud platforms.
|
| 13 |
+
- **Interactive Data Handling** – Uses `pandas`, `numpy`, and `scikit-learn` for data processing and ML.
|
| 14 |
+
- **Spotify Integration (Optional)** – Pulls metadata via [Spotipy](https://spotipy.readthedocs.io/) to enrich recommendations.
|
| 15 |
+
|
| 16 |
---
|
| 17 |
|
| 18 |
+
## 🏗️ Tech Stack
|
| 19 |
+
- **Backend:** FastAPI, Uvicorn
|
| 20 |
+
- **ML / Data Science:** Scikit-learn, Pandas, Numpy, Matplotlib
|
| 21 |
+
- **Deployment:** Docker, Render (or any cloud hosting)
|
| 22 |
+
- **Testing:** Pytest
|
data/processed/chord_exercises.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6411955b2b75db12e9636397d87fc97b144a9042834e26a6f0116cd1ebaf3d12
|
| 3 |
+
size 13013
|
data/processed/prod_data.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:73006bd788a51c4df96131171459c721b6bc187cb47c549c128998af458ec328
|
| 3 |
+
size 24639044
|
notebooks/data_visualisation.ipynb
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "88f47d25",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"import pandas as pd\n",
|
| 11 |
+
"import numpy as np\n",
|
| 12 |
+
"from pathlib import Path\n",
|
| 13 |
+
"from sklearn.metrics import silhouette_score\n",
|
| 14 |
+
"import matplotlib.pyplot as plt\n",
|
| 15 |
+
"from sklearn.decomposition import PCA\n",
|
| 16 |
+
"from sklearn.cluster import MiniBatchKMeans\n",
|
| 17 |
+
"from sklearn.metrics.pairwise import cosine_similarity"
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"cell_type": "code",
|
| 22 |
+
"execution_count": null,
|
| 23 |
+
"id": "b34b67b4",
|
| 24 |
+
"metadata": {},
|
| 25 |
+
"outputs": [],
|
| 26 |
+
"source": [
|
| 27 |
+
"marks_file_path = Path(\"..\",\"data\",\"processed\",\"marks_data.parquet\")#songs_df = pd.read_parquet(marks_file_path)\n",
|
| 28 |
+
"exercise_data_path = Path(\"..\",\"data\",\"processed\",\"chord_exercises.parquet\")\n",
|
| 29 |
+
"exercise_df = pd.read_parquet(exercise_data_path)\n",
|
| 30 |
+
"prod_data_path = Path(\"..\",\"data\",\"processed\",\"prod_data.parquet\")\n",
|
| 31 |
+
"prod_data = pd.read_parquet(prod_data_path)"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"cell_type": "code",
|
| 36 |
+
"execution_count": 6,
|
| 37 |
+
"id": "2422497a",
|
| 38 |
+
"metadata": {},
|
| 39 |
+
"outputs": [
|
| 40 |
+
{
|
| 41 |
+
"name": "stdout",
|
| 42 |
+
"output_type": "stream",
|
| 43 |
+
"text": [
|
| 44 |
+
"Running PCA to reduce from 3024 → 100 dimensions...\n",
|
| 45 |
+
"PCA complete. Shape: (168630, 100)\n",
|
| 46 |
+
"Explained variance ratio (first 10 comps): [0.13954444 0.10823043 0.06132166 0.04828419 0.03467049 0.03131611\n",
|
| 47 |
+
" 0.02613195 0.02348917 0.02307746 0.02195262]\n",
|
| 48 |
+
"Running clustering for different k values...\n",
|
| 49 |
+
"k=2 → inertia=695637.38, silhouette=0.1310\n",
|
| 50 |
+
"k=3 → inertia=644517.48, silhouette=0.1130\n",
|
| 51 |
+
"k=4 → inertia=618581.60, silhouette=0.1005\n",
|
| 52 |
+
"k=5 → inertia=585383.74, silhouette=0.1049\n",
|
| 53 |
+
"k=6 → inertia=566055.58, silhouette=0.1035\n",
|
| 54 |
+
"k=7 → inertia=545570.39, silhouette=0.1041\n",
|
| 55 |
+
"k=8 → inertia=536465.33, silhouette=0.1070\n",
|
| 56 |
+
"k=9 → inertia=532107.15, silhouette=0.0945\n",
|
| 57 |
+
"k=10 → inertia=528159.87, silhouette=0.0938\n",
|
| 58 |
+
"k=11 → inertia=517973.04, silhouette=0.0972\n",
|
| 59 |
+
"k=12 → inertia=510710.85, silhouette=0.0988\n",
|
| 60 |
+
"k=13 → inertia=505189.73, silhouette=0.1004\n",
|
| 61 |
+
"k=14 → inertia=498099.02, silhouette=0.1037\n",
|
| 62 |
+
"k=15 → inertia=485065.30, silhouette=0.0970\n",
|
| 63 |
+
"k=16 → inertia=476857.74, silhouette=0.0964\n",
|
| 64 |
+
"k=17 → inertia=472460.50, silhouette=0.0972\n",
|
| 65 |
+
"k=18 → inertia=468655.84, silhouette=0.0975\n",
|
| 66 |
+
"k=19 → inertia=467330.06, silhouette=0.0960\n"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"data": {
|
| 71 |
+
"image/png": "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joyMFAcMGCCam5uLRkZGYps2bcSdO3cW+uydO3fE3r17i8bGxqKVlZU4ceJEMSQkpNjln4mIiKqr3bt3i++++67YsGFD0cTERDQwMBDd3d3FCRMmiAkJCVpjnZ2dxWHDhknvDxw4UOje2rFjR7Fx48aFzjNs2DDR2dlZa1tCQoI4YsQI0crKSjQwMBC9vLy0nqcKPHz4UOzfv79obGws1qpVS3zvvffES5cuFXr+EsWXfy6Jj48Xe/bsKZqamooAtJ5FHj9+LAYHB4vu7u6igYGBaGVlJb7yyivivHnzxJycnGKv8bOuy9MKrueWLVsKXadx48aJTk5Oor6+vmhrayt27txZXLFihda4P/74Q/Tw8BD19PS0rk1Z/E6SkpLEd955RzQzMxNVKpX4zjvviOfOnSvyd0JUFQmiyI5sRERERERERERUvthTioiIiIiIiIiIyh2TUkREREREREREVO6YlCIiIiIiIiIionLHpBQREREREREREZU7JqWIiIiIiIiIiKjcMSlFRERERERERETlTk/uAKoTjUaD+/fvw9TUFIIgyB0OERERvQRRFPH48WPY29tDoeD3fC+Lz0lERERVR0mfk5iUKkf379+Hk5OT3GEQERGRDt29exeOjo5yh1Hp8TmJiIio6nnecxKTUuXI1NQUQP4vxczMTOZoiIiI6GWkpaXByclJur/Ty+FzEhERUdVR0uckJqXKUUEpupmZGR+2iIiIqghONdMNPicRERFVPc97TmIDBCIiIiIiIiIiKndMShERERERERERUbljUoqIiIiIiIiIiMode0oRERFVImq1Grm5uXKHUS3o6+tDqVTKHQYREVGR+ExActLVc5KsSSkXFxfcuXOn0Pb//e9/WLJkCbKysvDhhx/il19+QXZ2NgICArB06VLY2NhIY2NiYjB27FgcOHAAJiYmGDZsGGbPng09vX9/tIMHDyIoKAiXL1+Gk5MTPvvsMwwfPlzrnEuWLME333yD+Ph4NG3aFN999x3atGkj7S9JLERERGVFFEXEx8cjJSVF7lCqFXNzc9ja2rKZORERVRh8JqCKQhfPSbImpU6fPg21Wi29v3TpErp06YI33ngDADBp0iT89ddf2LJlC1QqFcaPH49+/frh2LFjAPIzwz179oStrS2OHz+OuLg4DB06FPr6+vjqq68AAFFRUejZsyfef/99/PzzzwgNDcWoUaNgZ2eHgIAAAMDmzZsRFBSE5cuXw9vbGwsXLkRAQACuX78Oa2vrEsVCRERUlgoePq2trWFsbMwkSRkTRRGZmZl48OABAMDOzk7miIiIiPLxmYDkpsvnJEEURVFXgb2swMBA7Ny5Ezdv3kRaWhpq166NjRs3YsCAAQCAa9euoVGjRggLC4OPjw92796N1157Dffv35cqlpYvX45PPvkEDx8+hIGBAT755BP89ddfuHTpknSegQMHIiUlBSEhIQAAb29vtG7dGt9//z0AQKPRwMnJCRMmTMCUKVOQmpr63FhKIi0tDSqVCqmpqVzqmIiISkytVuPGjRuwtraGpaWl3OFUK0lJSXjw4AHq169fqESd93Xd4vUkIno+PhNQRaKL56QK0+g8JycHGzZswLvvvgtBEBAeHo7c3Fz4+/tLYxo2bIg6deogLCwMABAWFgYvLy+tKXQBAQFIS0vD5cuXpTFPH6NgTMExcnJyEB4erjVGoVDA399fGlOSWIiIiMpKQb8IY2NjmSOpfgquOXt2EBFRRcBnAqpIdPGcVGEanf/+++9ISUmRej3Fx8fDwMAA5ubmWuNsbGwQHx8vjflvT6eC988bk5aWhidPnuDRo0dQq9VFjrl27VqJYylKdnY2srOzpfdpaWnPuAJERETPxvL88sdrTkREFRHvT1QR6OL/wwpTKfXTTz+he/fusLe3lzsUnZk9ezZUKpX0cnJykjskIiIiIiIiIqIKoUIkpe7cuYN9+/Zh1KhR0jZbW1vk5OQUWlEgISEBtra20piEhIRC+wv2PWuMmZkZatSoASsrKyiVyiLHPH2M58VSlODgYKSmpkqvu3fvPudKEBERka4cPHgQgiBwdSIiIqJKQhAE/P777wCA6OhoCIKAiIgIALyvV1UVIim1evVqWFtbo2fPntK2li1bQl9fH6GhodK269evIyYmBr6+vgAAX19fXLx4Uer4DgB79+6FmZkZPDw8pDFPH6NgTMExDAwM0LJlS60xGo0GoaGh0piSxFIUQ0NDmJmZab3KglojIiwyCX9ExCIsMglqTYXpXU9ERBVMed8zhg8fjr59+5bpOQDAz88PgYGBWtteeeUVxMXFQaVSlfn5qeJSa9Q4GH0Qmy5uwsHog1Br1M//EBFRNVDefz8+fPgQY8eORZ06dWBoaAhbW1sEBARorWgfFxeH7t27l2kculDUc4cuk2ZRUVEYPHgw7O3tYWRkBEdHR/Tp00dqMVSVyN5TSqPRYPXq1Rg2bBj09P4NR6VSYeTIkQgKCoKFhQXMzMwwYcIE+Pr6Sqvdde3aFR4eHnjnnXcwd+5cxMfH47PPPsO4ceNgaGgIAHj//ffx/fffY/LkyXj33Xexf/9+/Prrr/jrr7+kcwUFBWHYsGFo1aoV2rRpg4ULFyIjIwMjRowocSxyCbkUhxl/XkFcapa0zU5lhGm9PNDNk8tXExHRv6riPSMnJwcGBgZF7jMwMHhmRTNVfduubsPEkIm4l3ZP2uZo5ohF3RahX6N+MkZGRCQvOf5+7N+/P3JycrB27VrUrVsXCQkJCA0NRVJSkjSG9+38puFdunRBgwYNsG3bNtjZ2eHevXvYvXt3mVaJ5ebmQl9fv8yOXxzZK6X27duHmJgYvPvuu4X2LViwAK+99hr69++PDh06wNbWFtu2bZP2K5VK7Ny5E0qlEr6+vhgyZAiGDh2KL774Qhrj6uqKv/76C3v37kXTpk0xf/58/PjjjwgICJDGvPXWW5g3bx6mTp2KZs2aISIiAiEhIVrNz58XixxCLsVh7IazWv+4AID41CyM3XAWIZfiZIqMiIgqmopwz/Dz88MHH3yAyZMnw8LCAra2tpg+fbrWmJSUFIwaNQq1a9eGmZkZXn31VZw/f17aP336dDRr1gw//vgjXF1dYWRkhOHDh+PQoUNYtGgRBEGAIAiIjo4u9I1lUlISBg0aBAcHBxgbG8PLywubNm0q85+b5LHt6jYM+HWA1j+4ACA2LRYDfh2AbVflfY4jIpKLHH8/pqSk4MiRI/j666/RqVMnODs7o02bNggODkbv3r2lcU9P3ytOeHg4WrVqBWNjY7zyyiu4fv261v5ly5bBzc0NBgYGaNCgAdavXy/t+++UwILYBEHAwYMHpW2XLl1C9+7dYWJiAhsbG7zzzjtITEwEgGKfOzp16gQAqFWrFgRBkBZx02g0mD17NlxdXVGjRg00bdoUW7duLfbnu3z5MiIjI7F06VL4+PjA2dkZbdu2xaxZs7SKYu7du4dBgwbBwsICNWvWRKtWrXDy5MkSXYeCa71s2TL07t0bNWvWxJdffgkA+OOPP9CiRQsYGRmhbt26mDFjBvLy8p75O3kZsldKde3aFaJY9NQBIyMjLFmyBEuWLCn2887Ozti1a9czz+Hn54dz5849c8z48eMxfvz4YveXJJbypNaImPHnFRR15UQAAoAZf15BFw9bKBVcmYGIqKoRRRFPcktWZq/WiJi24/Iz7xnTd1xBW3er594zaugrX2qllbVr1yIoKAgnT55EWFgYhg8fjrZt26JLly4AgDfeeAM1atTA7t27oVKp8MMPP6Bz5864ceMGLCwsAAC3bt3Cb7/9hm3btkGpVMLZ2Rk3btyAp6en9MVU7dq1ER0drXXurKwstGzZEp988gnMzMzw119/4Z133oGbmxvatGlT6p+JKh61Ro2JIRMhFvF/vQgRAgQEhgSiT4M+UCqUMkRIRKQ7oigiMzezRGPVGjU+2P3BM/9+nLh7Ivxd/Uv096OxvnGJngtMTExgYmKC33//HT4+PtLMptL49NNPMX/+fNSuXRvvv/8+3n33XWkK4Pbt2zFx4kQsXLgQ/v7+2LlzJ0aMGAFHR0cpafQ8KSkpePXVVzFq1CgsWLAAT548wSeffII333wT+/fvx6JFi4p87vjtt9/Qv39/XL9+XephDeQvgLZhwwYsX74c9erVw+HDhzFkyBDUrl0bHTt2LHT+2rVrQ6FQYOvWrQgMDIRSWfj3kJ6ejo4dO8LBwQE7duyAra0tzp49C41G80LXYfr06ZgzZw4WLlwIPT09HDlyBEOHDsXixYvRvn17REZGYsyYMQCAadOmvcBvqeRkT0pR6ZyKSi70bffTRABxqVk4FZUMXzfL8guMiIjKxZNcNTym7tHJsUQA8WlZ8Jr+93PHXvkiAMYGpX98aNKkifRQU69ePXz//fcIDQ1Fly5dcPToUZw6dQoPHjyQHlbnzZuH33//HVu3bpUeinJycrBu3TrUrl1bOq6BgQGMjY2fWfbv4OCAjz76SHo/YcIE7NmzB7/++iuTUlXMkZgjhSoAniZCxN20uzgScwR+Ln7lFxgRURnIzM2EyWwTnRxLhIh7j+9B9XXJ+jGmB6ejpkHN547T09PDmjVrMHr0aCxfvhwtWrRAx44dMXDgQDRp0uSFYvzyyy+lZM6UKVPQs2dPZGVlwcjICPPmzcPw4cPxv//9D0B+q54TJ05g3rx5JU5Kff/992jevDm++uoraduqVavg5OSEGzduoH79+kU+dxR8eWZtbQ1zc3MAQHZ2Nr766ivs27dP6kddt25dHD16FD/88EORSSkHBwcsXrwYkydPxowZM9CqVSt06tQJb7/9NurWrQsA2LhxIx4+fIjTp09L53V3d5eOUdLrMHjwYKltEQC8++67mDJlCoYNGybFOnPmTEyePLnMklKyT9+j0nnwuPiEVGnGERERlYf/Pnja2dlJC5acP38e6enpsLS0lL5RNTExQVRUFCIjI6XPODs7ayWkSkqtVmPmzJnw8vKChYUFTExMsGfPHsTExLzcD0UVTtzjkk1HLek4IiJ6ef3798f9+/exY8cOdOvWDQcPHkSLFi2wZs2aFzrO088Sdnb5PTELniWuXr2Ktm3bao1v27Ytrl69WuLjnz9/HgcOHNB6FmnYsCEAaD2PlMStW7eQmZmJLl26aB1v3bp1zzzWuHHjEB8fj59//hm+vr7YsmULGjdujL179wIAIiIi0Lx5cykh9V8lvQ6tWrUq9LN/8cUXWrGOHj0acXFxyMwsWTXei2KlVCVlbWqk03FERFS51NBX4soXAc8fiPzq2uGrTz933JoRrdHGteiHm6fP+zL+20BTEASp1Dw9PR12dnZaPR0KFHzjCAA1az7/G9mifPPNN1i0aBEWLlwILy8v1KxZE4GBgcjJySnV8ajisjMtWeP+ko4jIqrIjPWNkR6cXqKxh+8cRo+NPZ47btfgXejg3KFE534RRkZG6NKlC7p06YLPP/8co0aNwrRp06T+SyXx9LNEwdTBgmeJ51Eo8utynm4hlJubqzUmPT0dvXr1wtdff13o8wVJsJJKT8//vfz1119wcHDQ2ve8KYympqbo1asXevXqhVmzZiEgIACzZs1Cly5dpKmBL+u/z1Tp6emYMWMG+vUr3OzeyKhscgtMSlVSbVwtYKcyQnxqVpE9QgQAtiqj5/7jgoiIKidBEEo8ja59vdolume0r1db1j6ELVq0QHx8PPT09ODi4vJCnzUwMIBa/eweW8eOHUOfPn0wZMgQAPkPsDdu3ICHh0dpQ6YKqn2d9nA0c0RsWmyRfVMECHA0c0T7Ou1liI6ISLcEQSjRFDoA6OrWtUR/P3Z161ouPfc8PDye29j8RTRq1AjHjh2Tpp8B+ff/gnt9QaV1XFwcmjdvDgBaTc+B/OeR3377DS4uLtDTK/pZq6jnjoLVgJ/e7uHhAUNDQ8TExBQ5Va+kBEFAw4YNcfz4cQD51WI//vgjkpOTi6yWet51KE6LFi1w/fp1ramAZY3T9yoppULAtF75/0P9958PBe+n9fJgk3MiIqo09wx/f3/4+vqib9+++PvvvxEdHY3jx4/j008/xZkzZ575WRcXF5w8eRLR0dFITEws8hvTevXqYe/evTh+/DiuXr2K9957DwkJCWX145CMlAolFnVbBCD/H1hPK3i/sNtCNjknompHrr8fk5KS8Oqrr2LDhg24cOECoqKisGXLFsydOxd9+vTR2Xk+/vhjrFmzBsuWLcPNmzfx7bffYtu2bVJPyRo1asDHxwdz5szB1atXcejQIXz22Wdaxxg3bhySk5MxaNAgnD59GpGRkdizZw9GjBghJZyKeu5wdnaGIAjYuXMnHj58iPT0dJiamuKjjz7CpEmTsHbtWkRGRuLs2bP47rvvsHbt2iJ/hoiICPTp0wdbt27FlStXcOvWLfz0009YtWqVdK0GDRoEW1tb9O3bF8eOHcPt27fx22+/ISwsrETXoThTp07FunXrMGPGDFy+fBlXr17FL7/8Uuga6RKTUpVYN087LBvSArYq7TI6sxr6WDakBbp5siSdiIjyFXfPsFUZVZh7hiAI2LVrFzp06IARI0agfv36GDhwIO7cuQMbG5tnfvajjz6CUqmEh4cHateuXWSfqM8++wwtWrRAQEAA/Pz8pIc5qpr6NeqHrW9uhYOZ9nQJRzNHbH1zK/o1Kjw1gYioOpDj70cTExN4e3tjwYIF6NChAzw9PfH5559j9OjR+P7773V2nr59+2LRokWYN28eGjdujB9++AGrV6+Gn5+fNGbVqlXIy8tDy5YtERgYiFmzZmkdw97eHseOHYNarUbXrl3h5eWFwMBAmJubS9P/inrucHBwwIwZMzBlyhTY2Nhg/PjxAICZM2fi888/x+zZs9GoUSN069YNf/31F1xdXYv8GRwdHeHi4oIZM2bA29sbLVq0wKJFizBjxgx8+umnAPKrsv7++29YW1ujR48e8PLywpw5c6SV+kpyHYoSEBCAnTt34u+//0br1q3h4+ODBQsWwNnZuTS/jhIRxKcnU1KZSktLg0qlQmpqKszMzHR2XLVGxKmoZKwPi8auS/Hwq2+FNe966+z4REQkr6ysLERFRcHV1fWl5/MX3DMePM6CtWn+NG+5K6Qqsmdd+7K6r+vCkiVL8M033yA+Ph5NmzbFd999V+wKg5cvX8bUqVMRHh6OO3fuYMGCBQgMDNQas2zZMixbtgzR0dEAgMaNG2Pq1Kno3r27NCYrKwsffvghfvnlF2RnZyMgIABLly59bkKxQNk9J6nxv7/+hxVnV6CTSyfsfWcvK6SIqNLS7TOBGkdijiDucRzsTO3Qvk57/v1IL0QXz0mslKoClAoBvm6WCOpaHwBwLDIJqU9yn/MpIiKqjgruGX2aOcDXzZIJqSpo8+bNCAoKwrRp03D27Fk0bdoUAQEB0spE/5WZmYm6detizpw5WktbP83R0RFz5sxBeHg4zpw5g1dffRV9+vTB5cuXpTGTJk3Cn3/+iS1btuDQoUO4f/9+kY1Sy5tSoUSPevlNfVOzU/kPLiKifygVSvi5+GGQ1yD4ufjx70eSBZNSVYi7tSncrU2Qqxax/xp7ZBAREVVH3377LUaPHo0RI0bAw8MDy5cvh7GxMVatWlXk+NatW+Obb77BwIEDi10JqFevXujRowfq1auH+vXr48svv4SJiQlOnDgBAEhNTcVPP/2Eb7/9Fq+++ipatmyJ1atX4/jx49IYOdW3zP/i7kbSDXCSABERUcXBpFQV08Mz/xvOXRfjZY6EiIiIyltOTg7Cw8Ph7+8vbVMoFPD395ean74stVqNX375BRkZGfD19QUAhIeHIzc3V+u8DRs2RJ06dXR23pfhZuEGhaBAek464tLj5A6HiIiI/sGkVBVT0Kj20I2HSM/OkzkaIiIiKk+JiYlQq9WF+jjZ2NggPv7lvrC6ePEiTExMYGhoiPfffx/bt2+XlpaOj4+HgYEBzM3NS3ze7OxspKWlab3KioHSAK7m+Q1lrydeL7PzEBER0YthUqqKaWRnChdLY+TkaXDgWtG9I4iIiIheVIMGDRAREYGTJ09i7NixGDZsGK5cuVLq482ePRsqlUp6OTk56TDawhpYNQCQP4WPiIiIKgYmpaoYQRCkaqmQS5zCR0RUlWg0GrlDqHYq2zW3srKCUqlEQoJ2b8mEhIRim5iXlIGBAdzd3dGyZUvMnj0bTZs2xaJFiwAAtra2yMnJQUpKSonPGxwcjNTUVOl19+7dl4rveepb5PeVup7ESikiqvwq2/2JqiZd/H+op4M4qILp4WWL5Ycisf/aAzzJUaOGAVdRICKqzAwMDKBQKHD//n3Url0bBgYGEASumleWRFFETk4OHj58CIVCAQMDA7lDKhEDAwO0bNkSoaGh6Nu3L4D8B8bQ0FCMHz9ep+fSaDTIzs4GALRs2RL6+voIDQ1F//79AQDXr19HTEyM1HfqvwwNDYttrF4WWClFRFUBnwmoItDlcxKTUlWQl4MKDuY1EJvyBIduPEQ3z5f7ZpSIiOSlUCjg6uqKuLg43L9/X+5wqhVjY2PUqVMHCkXlKS4PCgrCsGHD0KpVK7Rp0wYLFy5ERkYGRowYAQAYOnQoHBwcMHv2bAD5zdELpuHl5OQgNjYWERERMDExgbu7O4D8qqbu3bujTp06ePz4MTZu3IiDBw9iz549AACVSoWRI0ciKCgIFhYWMDMzw4QJE+Dr6wsfHx8ZrkJhDSzzk1KslCKiyozPBFSR6OI5iUmpKkgQBHT3tMWPR6Ow+1Ick1JERFWAgYEB6tSpg7y8PKjVarnDqRaUSiX09PQq3TfQb731Fh4+fIipU6ciPj4ezZo1Q0hIiNT8PCYmRuvh8f79+2jevLn0ft68eZg3bx46duyIgwcPAgAePHiAoUOHIi4uDiqVCk2aNMGePXvQpUsX6XMLFiyAQqFA//79kZ2djYCAACxdurR8fugSqG+ZP30v6lEUctQ5MFBWjuo3IqL/4jMBVQS6ek4SRFEUdRQTPUdaWhpUKhVSU1NhZmZWpucKv5OM/svCYGKoh/DP/WGoxyl8REREulSe9/XqoKyvpyiKMJ1tiozcDFwddxUNrRrq/BxERESUr6T39cpTi04vpLlTLdiYGSI9Ow9HbybKHQ4RERGRrARBkKqlridyCh8REVFFwKRUFaVQCOjWOH/a3m6uwkdERETEZudEREQVDJNSVVh3LzsAwN4rCchVc8lQIiIiqt7qW/xTKcVm50RERBUCk1JVWGsXC1iZGCD1SS7CIpPkDoeIiIhIVqyUIiIiqliYlKrClAoBXaUpfHEyR0NEREQkrwaW+UkpVkoRERFVDExKVXHdPfOTUn9fTkAep/ARERFRNVbPsh4A4EHGA6RkpcgbDBERETEpVdX51LWEubE+kjJycCo6We5wiIiIiGRjZmgGO5P8npucwkdERCQ/JqWqOH2lAl0a2QAAQrgKHxEREVVz9S3/aXaeyCl8REREcmNSqhro8c8qfCGX4qHRiDJHQ0RERCSfgr5SrJQiIiKSH5NS1cAr7pYwNdTDg8fZOBvzSO5wiIiIiGQjVUqx2TkREZHsmJSqBgz1lPD3yJ/Ct+sip/ARERFR9dXAipVSREREFQWTUtVEt39W4Qu5FAdR5BQ+IiIiqp4KKqVuJN2ARuTKxERERHJiUqqa6Fi/NowNlLifmoXz91LlDoeIiIhIFq7mrtBT6OFJ3hPEpsXKHQ4REVG1xqRUNWGkr0SnhtYAgN2X4mSOhoiIiEge+kp9uNVyA8C+UkRERHJjUqoa6eGZvwrf7ovxnMJHRERE1ZbU7DyRSSkiIiI5MSlVjfg1qA0jfQVikjNxJS5N7nCIiIiIZNHAks3OiYiIKgImpaqRmoZ66Fi/NoD8aikiIiKi6kiqlOL0PSIiIlkxKVXN9PDKn8K3i6vwERERUTXVwIqVUkRERBUBk1LVzKsNrWGgVOD2wwzcfJAudzhERERE5a6gUio6JRpZeVkyR0NERFR9MSlVzZga6aN9PSsAnMJHRERE1ZNNTRuYGZpBhIjI5Ei5wyEiIqq2mJSqhrp52gIAdl+KkzkSIiIiovInCILU7Jx9pYiIiOTDpFQ11MXDBnoKAdfiH+P2Q07hIyIioupHanaeyKQUERGRXJiUqobMjQ3g62YJANh9iVP4iIiIqPopqJS6kcxm50RERHJhUqqaKliFL4RJKSIiIqqGWClFREQkPyalqqmuHjZQCMDF2FTcTc6UOxwiIiKictXA6p9KqSRWShEREcmFSalqytLEEN6u+VP4WC1FRERE1U09i3oAgKQnSUjKTJI5GiIiouqJSalqrLtX/ip8u7gKHxEREVUzNQ1qwtHMEQCrpYiIiOTCpFQ1FtDYFoIAnItJQVzqE7nDISIiIipXUl+pJPaVIiIikgOTUtWYjZkRWtapBYBT+IiIiKj6kVbgY6UUERGRLJiUqua6/7MK324mpYiIiKiaKUhKsVKKiIhIHkxKVXPdPPP7Sp2OTsaDx1kyR0NERERUfqTpe4lMShEREcmBSalqzsG8Bpo6mUMUgb8vJ8gdDhEREVG5aWCVXyl1K/kW1Bq1zNEQERFVP0xKEbr/Uy21m6vwERERUTXirHKGgdIA2epsxKTGyB0OERFRtcOkFElJqRO3k5GckSNzNERERETlQ6lQwt3CHQCbnRMREcmBSSmCs2VNeNiZQa0RsfcKG54TERFR9SH1lWKzcyIionLHpBQBAHp4FUzhY1KKiIiIqo+CFfhYKUVERFT+mJQiAEA3TzsAwLFbiUh9kitzNERERETloyApxUopIiKi8sekFAEA3K1NUN/GBLlqEaFXuQofERERVQ/S9L1EJqWIiIjKG5NSJCmoltp1kVP4iIiIqHpoYJVfKXU37S4yczNljoaIiKh6YVKKJAV9pQ7ffIj07DyZoyEiIqLSWrJkCVxcXGBkZARvb2+cOnWq2LGXL19G//794eLiAkEQsHDhwkJjZs+ejdatW8PU1BTW1tbo27cvrl/Xrizy8/ODIAhar/fff1/XP5rOWdawRC2jWgCAm0k3ZY6GiIioemFSiiQNbEzhalUTOXka7L/2QO5wiIiIqBQ2b96MoKAgTJs2DWfPnkXTpk0REBCABw+KvrdnZmaibt26mDNnDmxtbYscc+jQIYwbNw4nTpzA3r17kZubi65duyIjI0Nr3OjRoxEXFye95s6dq/OfT9cEQZCqpdjsnIiIqHwxKUUSQRDQ3TP/YTTkUpzM0RAREVFpfPvttxg9ejRGjBgBDw8PLF++HMbGxli1alWR41u3bo1vvvkGAwcOhKGhYZFjQkJCMHz4cDRu3BhNmzbFmjVrEBMTg/DwcK1xxsbGsLW1lV5mZmY6//nKgtRXis3OiYiIyhWTUqSl+z99pQ5ce4gnOWqZoyEiIqIXkZOTg/DwcPj7+0vbFAoF/P39ERYWprPzpKamAgAsLCy0tv/888+wsrKCp6cngoODkZlZfI+m7OxspKWlab3kUrACHyuliIiIyheTUqTF08EMjrVq4EmuGoducAofERFRZZKYmAi1Wg0bGxut7TY2NoiP181CJhqNBoGBgWjbti08PT2l7YMHD8aGDRtw4MABBAcHY/369RgyZEixx5k9ezZUKpX0cnJy0kl8pVGQlGKlFBERUfnSkzsAqlgKpvCtPBKFXRfjpRX5iIiIiABg3LhxuHTpEo4ePaq1fcyYMdKfvby8YGdnh86dOyMyMhJubm6FjhMcHIygoCDpfVpammyJqYLpezeSbkAURQiCIEscRERE1Q0rpaiQ7l75iaj91x4gK5dT+IiIiCoLKysrKJVKJCQkaG1PSEgoton5ixg/fjx27tyJAwcOwNHR8Zljvb29AQC3bt0qcr+hoSHMzMy0XnJxt3CHAAEpWSl4mPlQtjiIiIiqGyalqJBmjuawNTNCenYejt5MlDscIiIiKiEDAwO0bNkSoaGh0jaNRoPQ0FD4+vqW+riiKGL8+PHYvn079u/fD1dX1+d+JiIiAgBgZ1fxq65r6NdAHVUdAMD1RE7hIyIiKi9MSlEhCoWAbv+swrf7km76TxAREVH5CAoKwsqVK7F27VpcvXoVY8eORUZGBkaMGAEAGDp0KIKDg6XxOTk5iIiIQEREBHJychAbG4uIiAitCqdx48Zhw4YN2LhxI0xNTREfH4/4+Hg8efIEABAZGYmZM2ciPDwc0dHR2LFjB4YOHYoOHTqgSZMm5XsBSqmBFZudExERlTcmpahI3f9JSu29Eo+cPI3M0RAREVFJvfXWW5g3bx6mTp2KZs2aISIiAiEhIVLz85iYGMTFxUnj79+/j+bNm6N58+aIi4vDvHnz0Lx5c4waNUoas2zZMqSmpsLPzw92dnbSa/PmzQDyK7T27duHrl27omHDhvjwww/Rv39//Pnnn+X7w7+E+hb5faXY7JyIiKj8sNE5FamViwWsTAyRmJ6NsNtJ6Fi/ttwhERERUQmNHz8e48ePL3LfwYMHtd67uLhAFMVnHu95+52cnHDo0KEXirGiYaUUERFR+WOlFBVJqRAQ0Dj/G9XdF+OeM5qIiIiocitYgY+VUkREROWHSSkqVo9/VuH7+0oC8tScwkdERERVVwPL/EqpyORI5GnyZI6GiIioemBSiorl7WqBWsb6SM7IwamoZLnDISIiIiozTionGOkZIVeTi+iUaLnDISIiqhZkT0rFxsZiyJAhsLS0RI0aNeDl5YUzZ85I+4cPHw5BELRe3bp10zpGcnIy3n77bZiZmcHc3BwjR45Eenq61pgLFy6gffv2MDIygpOTE+bOnVsoli1btqBhw4YwMjKCl5cXdu3apbVfFEVMnToVdnZ2qFGjBvz9/XHz5k0dXo2KRU+pQFcPrsJHREREVZ9CUKCeRT0AwPVETuEjIiIqD7ImpR49eoS2bdtCX18fu3fvxpUrVzB//nzUqlVLa1y3bt0QFxcnvTZt2qS1/+2338bly5exd+9e7Ny5E4cPH8aYMWOk/WlpaejatSucnZ0RHh6Ob775BtOnT8eKFSukMcePH8egQYMwcuRInDt3Dn379kXfvn1x6dIlaczcuXOxePFiLF++HCdPnkTNmjUREBCArKysMrpC8uvulZ+UCrkcD43m2U1OiYiIiCozNjsnIiIqX7Kuvvf111/DyckJq1evlra5uroWGmdoaAhbW9sij3H16lWEhITg9OnTaNWqFQDgu+++Q48ePTBv3jzY29vj559/Rk5ODlatWgUDAwM0btwYERER+Pbbb6Xk1aJFi9CtWzd8/PHHAICZM2di7969+P7777F8+XKIooiFCxfis88+Q58+fQAA69atg42NDX7//XcMHDhQp9emonjFzQqmRnp4+Dgb4TGP0NrFQu6QiIiIiMpEfQs2OyciIipPslZK7dixA61atcIbb7wBa2trNG/eHCtXriw07uDBg7C2tkaDBg0wduxYJCUlSfvCwsJgbm4uJaQAwN/fHwqFAidPnpTGdOjQAQYGBtKYgIAAXL9+HY8ePZLG+Pv7a503ICAAYWFhAICoqCjEx8drjVGpVPD29pbG/Fd2djbS0tK0XpWNgZ4CXTzyV+HbxVX4iIiIqApjpRQREVH5kjUpdfv2bSxbtgz16tXDnj17MHbsWHzwwQdYu3atNKZbt25Yt24dQkND8fXXX+PQoUPo3r071Go1ACA+Ph7W1tZax9XT04OFhQXi4+OlMTY2NlpjCt4/b8zT+5/+XFFj/mv27NlQqVTSy8nJqeQXpwLp7pm/Cl/IJU7hIyIioqqrviUrpYiIiMqTrNP3NBoNWrVqha+++goA0Lx5c1y6dAnLly/HsGHDAEBrWpyXlxeaNGkCNzc3HDx4EJ07d5Yl7pIKDg5GUFCQ9D4tLa1SJqba17NCTQMl4lKzcP5eCprXqfX8DxERERFVMg0s8yul7j++j/ScdJgYmMgcERERUdUma6WUnZ0dPDw8tLY1atQIMTExxX6mbt26sLKywq1btwAAtra2ePDggdaYvLw8JCcnS32obG1tkZCQoDWm4P3zxjy9/+nPFTXmvwwNDWFmZqb1qoyM9JV4tVF+hVgIV+EjIiKiKqpWjVqobVwbAKfwERERlQdZk1Jt27bF9eva5dE3btyAs7NzsZ+5d+8ekpKSYGeXP6XM19cXKSkpCA8Pl8bs378fGo0G3t7e0pjDhw8jNzdXGrN37140aNBAWunP19cXoaGhWufau3cvfH19AeQ3YLe1tdUak5aWhpMnT0pjqrIenvmJt12X4iCKnMJHREREVVPBFD4mpYiIiMqerEmpSZMm4cSJE/jqq69w69YtbNy4EStWrMC4ceMAAOnp6fj4449x4sQJREdHIzQ0FH369IG7uzsCAgIA5FdWdevWDaNHj8apU6dw7NgxjB8/HgMHDoS9vT0AYPDgwTAwMMDIkSNx+fJlbN68GYsWLdKaWjdx4kSEhIRg/vz5uHbtGqZPn44zZ85g/PjxAABBEBAYGIhZs2Zhx44duHjxIoYOHQp7e3v07du3fC+cDDo2qA0jfQXuJj/BplMx+CMiFmGRSVCzxxQRERFVIQVT+K4nsq8UERFRWZO1p1Tr1q2xfft2BAcH44svvoCrqysWLlyIt99+GwCgVCpx4cIFrF27FikpKbC3t0fXrl0xc+ZMGBoaSsf5+eefMX78eHTu3BkKhQL9+/fH4sWLpf0qlQp///03xo0bh5YtW8LKygpTp07FmDFjpDGvvPIKNm7ciM8++wz/93//h3r16uH333+Hp6enNGby5MnIyMjAmDFjkJKSgnbt2iEkJARGRkblcLXkZWygh0a2Zjh3NwX/t/2StN1OZYRpvTzQ7Z9m6ERERESVGZudExERlR9B5FyscpOWlgaVSoXU1NRK118q5FIc3t9wttB24Z//LhvSgokpIiKqVirzfb0iqijX8/drv+P1za+jpV1LnBlzRrY4iIiIKrOS3tdlnb5HlYNaI2LGn1eK3FeQ0Zzx5xVO5SMiIqJK7+lKKX53S0REVLaYlKLnOhWVjLjUrGL3iwDiUrNwKiq5/IIiIiIiKgNutdygEBRIz0lHfDpXHSYiIipLTErRcz14XHxCqjTjiIiIiCoqQz1DuJi7AGBfKSIiorLGpBQ9l7VpyRq5l3QcERERUUVWsALfjaQbMkdCRERUtTEpRc/VxtUCdiojqan5fwnIX4WvjatFeYZFREREVCYKklLXE1kpRUREVJaYlKLnUioETOvlAQBFJqZEANN6eUCpKC5tRURERFR5PN3snIiIiMoOk1JUIt087bBsSAvYqgpP0WviqEJAY1sZoiIiIiLSvQZWnL5HRERUHvTkDoAqj26edujiYYtTUcl48DgL2bkaBG+7gAv3UrH7Ujx6eNnJHSIRERHRSyuolLr96DZy1DkwUBrIHBEREVHVxKQUvRClQoCvm6X0/t6jTCzefwtT/7iMV9wsYW7MhzYiIiKq3BxMHWCsb4zM3ExEPYqSKqeIiIhItzh9j17KuFfd4W5tgsT0bHz511W5wyEiIiJ6aYIgsK8UERFROWBSil6KoZ4SX/f3giAAW8Lv4ejNRLlDIiIiInppBSvwsa8UERFR2WFSil5aS2cLvOPjDAD4v+0X8SRHLXNERERERC+nICl1PZGVUkRERGWFSSnSicndGsJOZYSY5Ews2MdvFImIiKhyK5i+dyOZzzVERERlhUkp0gkTQz18+bonAODHI7dx4V6KvAERERERvYSC5uaslCIiIio7TEqRzrza0Aa9m9pDIwKTt15Arlojd0hEREREpVJQKZWQkYDUrFSZoyEiIqqamJQinZraywPmxvq4Fv8YKw7fljscIiIiolIxMzSDrYktADY7JyIiKitMSpFOWZkYYuprHgCARaE3EfkwXeaIiIiIiEqnoFrqehKn8BEREZUFJqVI515v7oAO9WsjJ0+D4G0XodGIcodERERE9MIKVuBjpRQREVHZYFKKdE4QBHzZ1xPGBkqcikrGptMxcodERERE9MIKklKslCIiIiobTEpRmXCyMMZHXfMf5Obsuob41CyZIyIiIiJ6MQXT91gpRUREVDaYlKIyM+wVFzRzMsfj7Dx89vsliCKn8REREVHl0cDq3+l7GpGrChMREekak1JUZpQKAV/3bwJ9pYB9VxOw62K83CERERERlZiruSv0FHrIzM3E/cf35Q6HiIioymFSispUA1tTjPVzBwBM23EJKZk5MkdEREREVDL6Sn3UrVUXAHA9kX2liIiIdI1JKSpz4zq5wd3aBInpOfjyr6tyh0NERERUYgV9pdjsnIiISPeYlKIyZ6inxNf9vSAIwJbwezh6M1HukIiIiKq0JUuWwMXFBUZGRvD29sapU6eKHXv58mX0798fLi4uEAQBCxcuLDRm9uzZaN26NUxNTWFtbY2+ffvi+nXtJE1WVhbGjRsHS0tLmJiYoH///khISND1j1buClbgY7NzIiIi3WNSispFS2cLvOPjDAAI3n4BT3LUMkdERERUNW3evBlBQUGYNm0azp49i6ZNmyIgIAAPHjwocnxmZibq1q2LOXPmwNbWtsgxhw4dwrhx43DixAns3bsXubm56Nq1KzIyMqQxkyZNwp9//oktW7bg0KFDuH//Pvr161cmP2N5YqUUERFR2RFELolWbtLS0qBSqZCamgozMzO5wyl36dl56PrtIdxPzcLo9q74tKeH3CERERGVWkW9r3t7e6N169b4/vvvAQAajQZOTk6YMGECpkyZ8szPuri4IDAwEIGBgc8c9/DhQ1hbW+PQ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H6v6GDBggPjll1+qnm/dulU0MjISf/jhB/HKlSvi1KlTRQsLCzEtLU3n51cTlV2LwsJCcfjw4aKDg4MYGxur9n9IQUGBqo8nr0VVP2t1VWXXIicnR3znnXfEqKgoMSEhQTx8+LDo6ekptmvXTszPz1f10RjeF6WysrLEpk2bimvXrtXYR0N5X1DDxnFSGY6TyuM4ieMkjpPKcJxUhuOk2sGkVAMFQONj48aNqjZ9+/YVJ06cqHo+e/Zs0cnJSTQ0NBRtbGzEF154QYyJidF98Fo2ZswYUS6Xi4aGhqK9vb04ZswY8fr166rXn7wOoiiK27dvF9u3by8aGhqKXbp0EX/99VcdR127fv/9dxGAGB8fX+61hvy+CA8P1/hzUXq+SqVSXLRokWhjYyMaGRmJAwcOLHeNnJ2dxZCQELVtX375peoa9ezZUzx58qSOzujpVXYtEhISKvw/JDw8XNXHk9eiqp+1uqqya5GXlyc+//zzorW1tWhgYCA6OzuLU6ZMKTdoagzvi1Lr168XTUxMxMzMTI19NJT3BTVsHCeV4TipPI6TOE7iOKkMx0llOE6qHYIoiuLTzrIiIiIiIiIiIiJ6GqwpRUREREREREREOsekFBERERERERER6RyTUkREREREREREpHNMShERERERERERkc4xKUVERERERERERDrHpBQREREREREREekck1JERERERERERKRzTEoREREREREREZHOMSlFREREREREREQ6x6QUEdV5iYmJEAQBsbGx+g5FJS4uDj4+PjA2NoaHh0eN96+L5/SsvvvuOzz//POq56+99hpGjBhRYft169Zh2LBhOoiMiIio4aqLYwqOk8rjOIlIMyaliKhKr732GgRBwPLly9W27969G4Ig6Ckq/QoJCYGpqSni4+MRFham73Dwww8/wMLCQm/Hz8/Px6JFixASElLtfV5//XXExMQgMjKyFiMjIiKqXRwnlcdxkjqOk4gqxqQUEVWLsbExVqxYgYcPH+o7FK0pLCx86n1v3LiBPn36wNnZGVZWVlqMSr8UCgWUSmWN99u5cyfMzc3Ru3fvau9jaGiIl19+Gf/9739rfDwiIqK6hOMkdRwnqeM4iahiTEoRUbX4+/vD1tYWy5Ytq7DN4sWLy03RXrVqFVxcXFTPS6cqL126FDY2NrCwsMCHH36I4uJivPvuu7C0tISD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8d1332ls7+3tjaZNm+Lf//43bty4gc2bN+OHH35Qvf748WPMnDkTERERuHXrFo4fP44zZ86oBpsuLi7Izc1FWFgY7t+/j7y8PLRv3x7jx4/HhAkTEBoaioSEBJw+fRrLli3Dr7/+WmHsCQkJWLhwIaKionDr1i388ccfuHbtmupYmgQEBODYsWMaX2vZsiWOHDmCuLg4jBs3DsXFxarXIiMj0bp160qn1xMREdUnHCdVjeOkMhwnUWPHpBQRPZUPP/yw3LTxTp064euvv8aaNWvg7u6O06dPP1NNhSctX74cy5cvh7u7O44dO4a9e/eiRYsWAKD61k6hUOD555+Hm5sbZs+eDQsLC7W6DNXx9ttvY+7cuZg3bx7c3Nxw8OBB7N27F+3atatRP4sWLcK8efPwwQcfoFOnThgzZky5+g+lLC0tsWnTJvz2229wc3PDli1b1JZQlkqlyMjIwIQJE9C+fXuMHj0agwcPxpIlSwAAvXr1wrRp0zBmzBhYW1vjk08+AVAyvXzChAmYN28eOnTogBEjRuDMmTNwcnKqMO6mTZsiLi4OI0eORPv27TF16lTMmDEDb775ZoX7TJ48Gb/99huysrI0vm5ra4sjR47g4sWLGD9+vGoAvGXLFkyZMqXS60hERFTfcJxUNY6TynCcRI2ZID55cy4REdFTeOmll+Dp6amxjoYmly9fxoABA/DXX39BJpPVcnRERERE+sNxEpFmnClFRERasXLlSo2r+lQkNTUVP/30EwdaRERE1OBxnESkGWdKERERERERERGRznGmFBERERERERER6RyTUkREREREREREpHNMShERERERERERkc4xKUVERERERERERDrHpBQREREREREREekck1JERERERERERKRzTEoREREREREREZHOMSlFREREREREREQ6x6QUERERERERERHp3P8D78s6bgT+QBgAAAAASUVORK5CYII=",
|
| 72 |
+
"text/plain": [
|
| 73 |
+
"<Figure size 1200x500 with 2 Axes>"
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
"metadata": {},
|
| 77 |
+
"output_type": "display_data"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"name": "stdout",
|
| 81 |
+
"output_type": "stream",
|
| 82 |
+
"text": [
|
| 83 |
+
"Final model trained with k=5\n",
|
| 84 |
+
"Centroids shape: (5, 100)\n"
|
| 85 |
+
]
|
| 86 |
+
}
|
| 87 |
+
],
|
| 88 |
+
"source": [
|
| 89 |
+
"\n",
|
| 90 |
+
"X = np.array(songs_df['feature_vector'].to_list())\n",
|
| 91 |
+
"\n",
|
| 92 |
+
"# --------------------------\n",
|
| 93 |
+
"# Step 1: Dimensionality Reduction with PCA\n",
|
| 94 |
+
"pca_components = 100 # adjust based on explained variance\n",
|
| 95 |
+
"print(f\"Running PCA to reduce from {X.shape[1]} → {pca_components} dimensions...\")\n",
|
| 96 |
+
"pca = PCA(n_components=pca_components, random_state=42)\n",
|
| 97 |
+
"X_reduced = pca.fit_transform(X)\n",
|
| 98 |
+
"\n",
|
| 99 |
+
"print(f\"PCA complete. Shape: {X_reduced.shape}\")\n",
|
| 100 |
+
"print(f\"Explained variance ratio (first 10 comps): {pca.explained_variance_ratio_[:10]}\")\n",
|
| 101 |
+
"\n",
|
| 102 |
+
"# --------------------------\n",
|
| 103 |
+
"# Step 2: Find optimal K with inertia + silhouette\n",
|
| 104 |
+
"inertias = []\n",
|
| 105 |
+
"silhouettes = []\n",
|
| 106 |
+
"K_range = range(2, 20) # You can extend this if needed\n",
|
| 107 |
+
"\n",
|
| 108 |
+
"print(\"Running clustering for different k values...\")\n",
|
| 109 |
+
"for k in K_range:\n",
|
| 110 |
+
" kmeans = MiniBatchKMeans(n_clusters=k, random_state=42, batch_size=2048, n_init=\"auto\")\n",
|
| 111 |
+
" labels = kmeans.fit_predict(X_reduced)\n",
|
| 112 |
+
" \n",
|
| 113 |
+
" inertia = kmeans.inertia_\n",
|
| 114 |
+
" inertias.append(inertia)\n",
|
| 115 |
+
" \n",
|
| 116 |
+
" sil_score = silhouette_score(X_reduced, labels, sample_size=10000, random_state=42)\n",
|
| 117 |
+
" silhouettes.append(sil_score)\n",
|
| 118 |
+
" \n",
|
| 119 |
+
" print(f\"k={k} → inertia={inertia:.2f}, silhouette={sil_score:.4f}\")\n",
|
| 120 |
+
"\n",
|
| 121 |
+
"# --------------------------\n",
|
| 122 |
+
"# Step 3: Plot inertia & silhouette to decide optimal k\n",
|
| 123 |
+
"plt.figure(figsize=(12,5))\n",
|
| 124 |
+
"\n",
|
| 125 |
+
"plt.subplot(1,2,1)\n",
|
| 126 |
+
"plt.plot(K_range, inertias, 'o-', label='Inertia')\n",
|
| 127 |
+
"plt.xlabel(\"Number of clusters (k)\")\n",
|
| 128 |
+
"plt.ylabel(\"Inertia\")\n",
|
| 129 |
+
"plt.title(\"Elbow Method\")\n",
|
| 130 |
+
"plt.legend()\n",
|
| 131 |
+
"\n",
|
| 132 |
+
"plt.subplot(1,2,2)\n",
|
| 133 |
+
"plt.plot(K_range, silhouettes, 'o-', color='green', label='Silhouette Score')\n",
|
| 134 |
+
"plt.xlabel(\"Number of clusters (k)\")\n",
|
| 135 |
+
"plt.ylabel(\"Silhouette\")\n",
|
| 136 |
+
"plt.title(\"Silhouette Method\")\n",
|
| 137 |
+
"plt.legend()\n",
|
| 138 |
+
"\n",
|
| 139 |
+
"plt.tight_layout()\n",
|
| 140 |
+
"plt.show()\n",
|
| 141 |
+
"\n",
|
| 142 |
+
"# --------------------------\n",
|
| 143 |
+
"# Step 4: Fit final model with optimal k (replace with your choice)\n",
|
| 144 |
+
"optimal_k = 5 # <-- set manually after looking at the plots\n",
|
| 145 |
+
"final_kmeans = MiniBatchKMeans(n_clusters=optimal_k, random_state=42, batch_size=2048, n_init=\"auto\")\n",
|
| 146 |
+
"final_labels = final_kmeans.fit_predict(X_reduced)\n",
|
| 147 |
+
"\n",
|
| 148 |
+
"# Get final centroids in PCA space\n",
|
| 149 |
+
"centroids = final_kmeans.cluster_centers_\n",
|
| 150 |
+
"\n",
|
| 151 |
+
"print(f\"Final model trained with k={optimal_k}\")\n",
|
| 152 |
+
"print(\"Centroids shape:\", centroids.shape)\n"
|
| 153 |
+
]
|
| 154 |
+
}
|
| 155 |
+
],
|
| 156 |
+
"metadata": {
|
| 157 |
+
"kernelspec": {
|
| 158 |
+
"display_name": "Python 3",
|
| 159 |
+
"language": "python",
|
| 160 |
+
"name": "python3"
|
| 161 |
+
},
|
| 162 |
+
"language_info": {
|
| 163 |
+
"codemirror_mode": {
|
| 164 |
+
"name": "ipython",
|
| 165 |
+
"version": 3
|
| 166 |
+
},
|
| 167 |
+
"file_extension": ".py",
|
| 168 |
+
"mimetype": "text/x-python",
|
| 169 |
+
"name": "python",
|
| 170 |
+
"nbconvert_exporter": "python",
|
| 171 |
+
"pygments_lexer": "ipython3",
|
| 172 |
+
"version": "3.11.9"
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
"nbformat": 4,
|
| 176 |
+
"nbformat_minor": 5
|
| 177 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
scikit-learn
|
| 4 |
+
pandas
|
| 5 |
+
numpy
|
| 6 |
+
pyarrow
|
src/__init__.py
ADDED
|
File without changes
|
src/data_input.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from logger import get_logger # pylint: disable=import-error
|
| 3 |
+
|
| 4 |
+
logger = get_logger(__name__)
|
| 5 |
+
|
| 6 |
+
def read_chord_data(url : str) -> pd.DataFrame:
|
| 7 |
+
"""
|
| 8 |
+
Function to read songs chord ata
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
url (str): Web api link to chord dataset
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
pd.DataFrame: Pandas read dataframe
|
| 15 |
+
"""
|
| 16 |
+
try:
|
| 17 |
+
logger.info('Reading Chord Data')
|
| 18 |
+
dataset = pd.read_csv(url, dtype=str)
|
| 19 |
+
return dataset
|
| 20 |
+
except Exception as e:
|
| 21 |
+
logger.error("Error reading chord data : %s", e)
|
| 22 |
+
raise
|
| 23 |
+
|
| 24 |
+
if __name__ == '__main__':
|
| 25 |
+
chord_data_url = "hf://datasets/ailsntua/Chordonomicon/chordonomicon_v2.csv"
|
| 26 |
+
df = read_chord_data(url= chord_data_url)
|
| 27 |
+
df.to_parquet("data/raw/songs_data.parquet", engine="pyarrow", index=False)
|
src/data_processing.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 5 |
+
from logger import get_logger # pylint: disable=import-error
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
logger = get_logger(__name__)
|
| 9 |
+
|
| 10 |
+
def clean_string(vars : list) -> list: # pylint: disable=redefined-builtin
|
| 11 |
+
"""
|
| 12 |
+
Funciton to manipulate and "clean" string values
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
vars (list): list of string variables to manipulate
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
list: cleaned list of strings
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
clean_vars = []
|
| 22 |
+
if isinstance(vars, str):
|
| 23 |
+
vars = [vars]
|
| 24 |
+
for s in vars:
|
| 25 |
+
temp = re.sub(r'[^a-z0-9\s]','',s.strip().lower())
|
| 26 |
+
temp = re.sub(r'\s+', '_', temp)
|
| 27 |
+
clean_vars.append(temp)
|
| 28 |
+
return clean_vars
|
| 29 |
+
except Exception as e:
|
| 30 |
+
logger.error("Error cleaning string : %s", e)
|
| 31 |
+
raise
|
| 32 |
+
|
| 33 |
+
def mandatory_column_check(df : pd.DataFrame, columns : list) -> bool:
|
| 34 |
+
"""
|
| 35 |
+
Function to check if the given columnsa re present in the dataframe
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
df (pd.DataFrame): Dataframe to check for columns presence
|
| 39 |
+
columns (list): Mandatory columns to check for
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
bool: Boolean value representing status
|
| 43 |
+
"""
|
| 44 |
+
try:
|
| 45 |
+
if isinstance(columns, str):
|
| 46 |
+
columns = [columns]
|
| 47 |
+
missing_col = []
|
| 48 |
+
df_headers_list = df.columns.to_list()
|
| 49 |
+
mandatory_columns = clean_string(vars = columns)
|
| 50 |
+
df_headers = clean_string(vars= df_headers_list)
|
| 51 |
+
for col in mandatory_columns:
|
| 52 |
+
if col.lower() not in [s.lower() for s in df_headers]:
|
| 53 |
+
missing_col.append(col)
|
| 54 |
+
if len(missing_col) >= 1:
|
| 55 |
+
return False
|
| 56 |
+
else:
|
| 57 |
+
return True
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error("Error checking for mandatory columns : %s", e)
|
| 60 |
+
raise
|
| 61 |
+
|
| 62 |
+
def drop_dataframe_columns(df : pd.DataFrame, columns: list) -> pd.DataFrame:
|
| 63 |
+
"""
|
| 64 |
+
Function to drop columns form a dataframe
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
df (pd.DataFrame): Dataframe to manipulate
|
| 68 |
+
columns (list): Columns to be dropped from the dataframe
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
pd.DataFrame: Formatted fataframe
|
| 72 |
+
"""
|
| 73 |
+
try:
|
| 74 |
+
final_df = df.drop(columns=columns)
|
| 75 |
+
return final_df
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error("Error droping columns : %s", e)
|
| 78 |
+
raise
|
| 79 |
+
|
| 80 |
+
def dataframe_join(df1 :pd.DataFrame, df2 : pd.DataFrame,
|
| 81 |
+
join_column: str) -> pd.DataFrame:
|
| 82 |
+
"""
|
| 83 |
+
Function to join different dataframes on a paticular column
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
df1 (pd.DataFrame): Left dataframe
|
| 87 |
+
df2 (pd.DataFrame): Right dataframe
|
| 88 |
+
join_column (str): Join column
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
pd.DataFrame: Cross join of both dataframes
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
formatted_join_col = clean_string(join_column)[0]
|
| 95 |
+
df_list = [df1, df2]
|
| 96 |
+
for i in range(len(df_list)): # pylint: disable=consider-using-enumerate
|
| 97 |
+
for col in df_list[i]:
|
| 98 |
+
formatted_col_name = clean_string(col)
|
| 99 |
+
df_list[i].rename(columns={col: formatted_col_name[0]}, inplace=True)
|
| 100 |
+
merged_df = pd.merge(df1, df2, on=formatted_join_col, how ='inner')
|
| 101 |
+
return merged_df
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error("Error merging dataframes : %s", e)
|
| 104 |
+
raise
|
| 105 |
+
|
| 106 |
+
def marks_custom_encoder(df : pd.DataFrame) -> pd.DataFrame:
|
| 107 |
+
"""
|
| 108 |
+
Function to implement custom one hot encoding for categorical variables
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
df (pd.DataFrame): Dataframe to manipulate
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
pd.DataFrame: Updated dataframe with one hot encoded columns
|
| 115 |
+
"""
|
| 116 |
+
try:
|
| 117 |
+
barre_chords_temp = ["F", "F♯", "G♯", "A♯", "B","Fm", "F♯m", "Gm", "G♯m","A♯m",
|
| 118 |
+
"Bm","Bb", "Cm", "C♯m", "D♯m","F7", "F♯7", "G♯7", "A♯7",
|
| 119 |
+
"C♯7", "D♯7","Fm7","F♯m7", "Gm7", "G♯m7", "Am7", "A♯m7",
|
| 120 |
+
"Bm7", "Cm7", "C♯m7", "Dm7", "D♯m7", "E7"]
|
| 121 |
+
major_minor_chords_temp = ["A", "C", "D", "E", "G", "Am", "Dm", "Em"]
|
| 122 |
+
special_chords = []
|
| 123 |
+
df['formatted_chords'] = df['chords'].apply(lambda x:
|
| 124 |
+
re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', '', x)).strip())
|
| 125 |
+
all_chords = set(chord for row in df['formatted_chords'] for chord in row.split()) # pylint: disable=redefined-outer-name
|
| 126 |
+
barre_chords = [x.lower() for x in barre_chords_temp]
|
| 127 |
+
barre_chords = [x for x in barre_chords if x not in major_minor_chords_temp]
|
| 128 |
+
major_minor_chords = [x.lower() for x in major_minor_chords_temp]
|
| 129 |
+
for chord in all_chords:
|
| 130 |
+
if chord.lower() not in barre_chords and chord.lower() not in major_minor_chords:
|
| 131 |
+
special_chords.append(chord)
|
| 132 |
+
|
| 133 |
+
df['song_length_temp'] = df['formatted_chords'].apply(lambda x: len(x)) # pylint: disable=unnecessary-lambda
|
| 134 |
+
df['song_length'] = (((df['song_length_temp'] - df['song_length_temp'].min())/
|
| 135 |
+
(df['song_length_temp'].max()-df['song_length_temp'].min()))) * 15
|
| 136 |
+
df['distinct_chords'] = df["formatted_chords"].apply(lambda x: set(x.split()))
|
| 137 |
+
df['barre_chords_metric'] = (df['distinct_chords'].apply(lambda x:
|
| 138 |
+
sum(chord in barre_chords_temp for chord in x)) * 2)
|
| 139 |
+
df['major_minor_chords_metric'] = df['distinct_chords'].apply(lambda x:
|
| 140 |
+
sum(chord in major_minor_chords_temp for chord in x))
|
| 141 |
+
df['special_chords'] = (df['distinct_chords'].apply(lambda x:
|
| 142 |
+
sum(chord in special_chords for chord in x)) * 3)
|
| 143 |
+
return df
|
| 144 |
+
except Exception as e:
|
| 145 |
+
logger.error("Error one hot encoding data : %s", e)
|
| 146 |
+
raise
|
| 147 |
+
|
| 148 |
+
def exercise_custom_encoder(df: pd.DataFrame)-> pd.DataFrame :
|
| 149 |
+
"""
|
| 150 |
+
Function to encode features in the exercise dataframe
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
df (pd.DataFrame): Dataframe to manipulate
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
pd.DataFrame: Resultant dtaframe
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
barre_chords_temp = ["F", "F♯", "G♯", "A♯", "B","Fm", "F♯m", "Gm", "G♯m","A♯m",
|
| 160 |
+
"Bm","Bb", "Cm", "C♯m", "D♯m","F7", "F♯7", "G♯7", "A♯7",
|
| 161 |
+
"C♯7", "D♯7","Fm7","F♯m7", "Gm7", "G♯m7", "Am7", "A♯m7",
|
| 162 |
+
"Bm7", "Cm7", "C♯m7", "Dm7", "D♯m7", "E7"]
|
| 163 |
+
major_minor_chords_temp = ["A", "C", "D", "E", "G", "Am", "Dm", "Em"]
|
| 164 |
+
special_chords = []
|
| 165 |
+
df_exploded = df['chord_progression'].str.split(',').explode()
|
| 166 |
+
all_chords = df_exploded.unique().tolist() # pylint: disable=redefined-outer-name
|
| 167 |
+
barre_chords = [x.lower() for x in barre_chords_temp]
|
| 168 |
+
barre_chords = [x for x in barre_chords if x not in major_minor_chords_temp]
|
| 169 |
+
major_minor_chords = [x.lower() for x in major_minor_chords_temp]
|
| 170 |
+
for chord in all_chords:
|
| 171 |
+
if chord.lower() not in barre_chords and chord.lower() not in major_minor_chords:
|
| 172 |
+
special_chords.append(chord)
|
| 173 |
+
df['barre_chords_metric'] = (df['chord_progression'].apply(lambda x:
|
| 174 |
+
sum(chord in barre_chords_temp for chord in x)) * 2)
|
| 175 |
+
df['major_minor_chords_metric'] = df['chord_progression'].apply(lambda x:
|
| 176 |
+
sum(chord in major_minor_chords_temp for chord in x))
|
| 177 |
+
df['special_chords'] = (df['chord_progression'].apply(lambda x:
|
| 178 |
+
sum(chord in special_chords for chord in x)) * 3)
|
| 179 |
+
df['tempo_mattric'] = ((df['tempo'] - 40) / (200 - 40))
|
| 180 |
+
return df
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error("Error encoding exercise data : %s", e)
|
| 183 |
+
raise
|
| 184 |
+
|
| 185 |
+
def get_universal_chords (df: pd.DataFrame) -> list:
|
| 186 |
+
"""
|
| 187 |
+
Function to get a list of all chords
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
df (pd.DataFrame): Dtaframe to extract chords cfrom
|
| 191 |
+
columns (list): Chord column name
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
list: List of all unqieu chords
|
| 195 |
+
"""
|
| 196 |
+
try:
|
| 197 |
+
df['formatted_chords'] = df['chords'].apply(lambda x:
|
| 198 |
+
re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', '', x)).strip())
|
| 199 |
+
all_chords_list = set(chord for row in df['formatted_chords'] for chord in row.split())
|
| 200 |
+
return all_chords_list
|
| 201 |
+
except Exception as e:
|
| 202 |
+
logger.error("Error getting universal chords list : %s", e)
|
| 203 |
+
raise
|
| 204 |
+
|
| 205 |
+
def chords_to_vector(chord_list, universal_chords):
|
| 206 |
+
"""
|
| 207 |
+
Convert list of chords into a binary vector based on universal chords.
|
| 208 |
+
"""
|
| 209 |
+
try:
|
| 210 |
+
return [1 if chord in chord_list else 0 for chord in universal_chords]
|
| 211 |
+
except Exception as e:
|
| 212 |
+
logger.error("Error in coverting chord to vector : 5s", e)
|
| 213 |
+
raise
|
| 214 |
+
|
| 215 |
+
def scaler_function(df:pd.DataFrame, columns:list)-> pd.DataFrame:
|
| 216 |
+
"""
|
| 217 |
+
Function to create and scale feature vectors
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
df (pd.DataFrame): Dataframe to manipulate
|
| 221 |
+
columns (list): Columns to engineer
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
pd.DataFrame: Resultant Dataframe
|
| 225 |
+
"""
|
| 226 |
+
try:
|
| 227 |
+
if isinstance(columns, str):
|
| 228 |
+
columns = [columns]
|
| 229 |
+
scaler = MinMaxScaler()
|
| 230 |
+
chords_scaled = scaler.fit_transform(df[columns])
|
| 231 |
+
df_scaled = pd.DataFrame(chords_scaled,columns=[c + "_scaled" for c in columns])
|
| 232 |
+
df = pd.concat([df.reset_index(drop=True), df_scaled.reset_index(drop=True)], axis=1)
|
| 233 |
+
return df
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error("Error in scaling columns: 5s", e)
|
| 236 |
+
raise
|
| 237 |
+
|
| 238 |
+
def create_feature_vector(df:pd.DataFrame, columns:list)-> pd.DataFrame:
|
| 239 |
+
"""
|
| 240 |
+
Function to create final feature vector
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
df (pd.DataFrame): Dataframe to manipulate
|
| 244 |
+
columns (list): _descriColumns to engineerption_
|
| 245 |
+
|
| 246 |
+
Returns:
|
| 247 |
+
pd.DataFrame: Resultant Dataframe
|
| 248 |
+
"""
|
| 249 |
+
try:
|
| 250 |
+
df['feature_vector'] = df.apply(
|
| 251 |
+
lambda row: row['chord_vector'] + [row[col] for col in columns],
|
| 252 |
+
axis=1)
|
| 253 |
+
return df
|
| 254 |
+
except Exception as e:
|
| 255 |
+
logger.error("Error in creating feature vectors: 5s", e)
|
| 256 |
+
raise
|
| 257 |
+
|
| 258 |
+
def exercise_build_vector(row): # pylint: disable=missing-function-docstring
|
| 259 |
+
try:
|
| 260 |
+
feature_cols = ['barre_chords_metric_scaled','major_minor_chords_metric_scaled',
|
| 261 |
+
'special_chords_scaled','tempo_mattric_scaled']
|
| 262 |
+
chord_vec = list(row['chord_vector'])
|
| 263 |
+
extra = [float(row[col]) for col in feature_cols]
|
| 264 |
+
return chord_vec + extra
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error("Error in building feature vector for exercise df: 5s", e)
|
| 267 |
+
raise
|
| 268 |
+
|
| 269 |
+
def marks_build_vector(row): # pylint: disable=missing-function-docstring
|
| 270 |
+
try:
|
| 271 |
+
feature_cols = ['barre_chords_metric_scaled','major_minor_chords_metric_scaled',
|
| 272 |
+
'special_chords_scaled','song_length_scaled']
|
| 273 |
+
chord_vec = list(row['chord_vector'])
|
| 274 |
+
extra = [float(row[col]) for col in feature_cols]
|
| 275 |
+
return chord_vec + extra
|
| 276 |
+
except Exception as e:
|
| 277 |
+
logger.error("Error in c\building feature vector for marks df: 5s", e)
|
| 278 |
+
raise
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
if __name__ == '__main__':
|
| 282 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 283 |
+
song_data_path = os.path.join(base_dir, '..','data','raw','songs_data.parquet')
|
| 284 |
+
spotify_data_path = os.path.join(base_dir,'..','data','raw','spotify_tracks.parquet')
|
| 285 |
+
exercise_data_path = os.path.join(base_dir, '..','data','raw','chord_exercises.csv')
|
| 286 |
+
|
| 287 |
+
song_data = pd.read_parquet(song_data_path)
|
| 288 |
+
spotify_data = pd.read_parquet(spotify_data_path)
|
| 289 |
+
exercise_data = pd.read_csv(exercise_data_path)
|
| 290 |
+
marks_data = dataframe_join(song_data, spotify_data, join_column='spotify_song_id')
|
| 291 |
+
marks_data = drop_dataframe_columns(marks_data, columns=['id','releasedate','decade',
|
| 292 |
+
'rockgenre','artistid',
|
| 293 |
+
'spotifysongid','spotifyartistid'])
|
| 294 |
+
marks_file_path = os.path.join(base_dir, '..', 'data', 'raw', 'marks_data.parquet')
|
| 295 |
+
marks_data_ohe = marks_custom_encoder(df=marks_data)
|
| 296 |
+
exercise_data_ohe = exercise_custom_encoder(exercise_data)
|
| 297 |
+
all_chords = get_universal_chords(marks_data_ohe)
|
| 298 |
+
marks_data_ohe['chord_vector'] = marks_data_ohe['distinct_chords'].apply(
|
| 299 |
+
lambda x: chords_to_vector(x, all_chords))
|
| 300 |
+
exercise_data_ohe['chord_vector'] = exercise_data_ohe['chord_progression'].apply(
|
| 301 |
+
lambda x: chords_to_vector(x.split(','), all_chords)
|
| 302 |
+
)
|
| 303 |
+
exercise_data_final = scaler_function(
|
| 304 |
+
df = exercise_data_ohe, columns=['barre_chords_metric',
|
| 305 |
+
'major_minor_chords_metric',
|
| 306 |
+
'special_chords','tempo_mattric'])
|
| 307 |
+
exercise_data_final['feature_vector'] = exercise_data_final.apply(exercise_build_vector, axis=1)
|
| 308 |
+
marks_data_final = scaler_function(df = marks_data_ohe, columns=
|
| 309 |
+
['barre_chords_metric', 'major_minor_chords_metric',
|
| 310 |
+
'special_chords','song_length'])
|
| 311 |
+
marks_data_final['feature_vector'] = marks_data_final.apply(marks_build_vector, axis=1)
|
| 312 |
+
|
| 313 |
+
marks_data_final.to_parquet(os.path.join(
|
| 314 |
+
base_dir, '..', 'data', 'processed', 'marks_data.parquet'))
|
| 315 |
+
exercise_data_ohe_path = os.path.join(
|
| 316 |
+
base_dir, '..','data','processed','chord_exercises.parquet')
|
| 317 |
+
exercise_data_final.to_parquet(exercise_data_ohe_path)
|
src/logger.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 5 |
+
|
| 6 |
+
LOG_FILE = os.path.join(BASE_DIR, "..", "logs","app.log")
|
| 7 |
+
|
| 8 |
+
# Configure logging
|
| 9 |
+
logging.basicConfig(
|
| 10 |
+
level=logging.INFO,
|
| 11 |
+
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
|
| 12 |
+
handlers=[
|
| 13 |
+
logging.FileHandler(LOG_FILE), # Writes logs to logs/app.log
|
| 14 |
+
logging.StreamHandler() # Also prints logs to console
|
| 15 |
+
]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Function to get logger for each module
|
| 19 |
+
def get_logger(name: str):
|
| 20 |
+
"""
|
| 21 |
+
Logger functoin to capture all data runs
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
name (str): Receives the log frome ach function
|
| 25 |
+
"""
|
| 26 |
+
return logging.getLogger(name)
|
src/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from fastapi import FastAPI, Query
|
| 5 |
+
import uvicorn
|
| 6 |
+
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 8 |
+
from model import recommend_songs as model1 # pylint: disable=import-error
|
| 9 |
+
from model import recommend_songs_random as model2 # pylint: disable=import-error
|
| 10 |
+
from logger import get_logger # pylint: disable=import-error
|
| 11 |
+
|
| 12 |
+
logger = get_logger(__name__)
|
| 13 |
+
|
| 14 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
data_dir = os.path.join(base_dir, "..", "data", "processed")
|
| 16 |
+
|
| 17 |
+
prod_file = os.path.join(data_dir, "prod_data.parquet")
|
| 18 |
+
exercise_file = os.path.join(data_dir, "chord_exercises.parquet")
|
| 19 |
+
recommended_history = set()
|
| 20 |
+
|
| 21 |
+
app = FastAPI(title="Exercise Recommendation API")
|
| 22 |
+
|
| 23 |
+
@app.get("/")
|
| 24 |
+
def home():
|
| 25 |
+
return {"message": "Welcome to the Exercise Recommendation API"}
|
| 26 |
+
|
| 27 |
+
@app.get("/random_exercises")
|
| 28 |
+
def random_exercises(genre: str = Query(..., description="Genre of exercises")):
|
| 29 |
+
"""Return n random exercises in batches to reduce memory usage."""
|
| 30 |
+
try:
|
| 31 |
+
recommended_temp = set()
|
| 32 |
+
prod_df = pd.read_parquet(
|
| 33 |
+
prod_file,
|
| 34 |
+
filters=[("maingenre", "=", genre)])
|
| 35 |
+
result = model2(genre=genre, songs_df=prod_df, recommended_cache=recommended_temp)
|
| 36 |
+
|
| 37 |
+
recommended_history.update(recommended_temp)
|
| 38 |
+
return result
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error("Error fetching API: %s", e)
|
| 41 |
+
return {"error": str(e)}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.get("/recommendations")
|
| 45 |
+
def recommendations(
|
| 46 |
+
tempo: int = Query(..., description="Tempo value"),
|
| 47 |
+
exercise_id: int = Query(..., description="Exercise ID"),
|
| 48 |
+
genre: str = Query(..., description="Genre"),
|
| 49 |
+
):
|
| 50 |
+
"""Return top N recommended songs for a given exercise and tempo using batch processing."""
|
| 51 |
+
try:
|
| 52 |
+
exercise_df = pd.read_parquet(
|
| 53 |
+
exercise_file,
|
| 54 |
+
filters=[("exercise_id", "=", exercise_id)],
|
| 55 |
+
)
|
| 56 |
+
prod_df = pd.read_parquet(
|
| 57 |
+
prod_file,
|
| 58 |
+
filters=[("maingenre", "=", genre)])
|
| 59 |
+
|
| 60 |
+
result = model1(
|
| 61 |
+
exercise_df=exercise_df,
|
| 62 |
+
prod_df=prod_df,
|
| 63 |
+
tempo=tempo,
|
| 64 |
+
exercise_id=exercise_id,
|
| 65 |
+
genre=genre,
|
| 66 |
+
)
|
| 67 |
+
return result
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.error("Error fetching API: %s", e)
|
| 71 |
+
return {"error": str(e)}
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
uvicorn.run("main:app", host="0.0.0.0", port=7860)
|
src/model.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from sklearn.decomposition import PCA
|
| 5 |
+
from sklearn.cluster import MiniBatchKMeans
|
| 6 |
+
|
| 7 |
+
from logger import get_logger # pylint: disable=import-error
|
| 8 |
+
|
| 9 |
+
logger = get_logger(__name__)
|
| 10 |
+
|
| 11 |
+
def cluster_function(df : pd.DataFrame)-> pd.DataFrame:
|
| 12 |
+
"""
|
| 13 |
+
Function to cluster marks dataframe
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
df (pd.DataFrame): Dataframe to predict
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
pd.DataFrame: Resultant dataframe
|
| 20 |
+
"""
|
| 21 |
+
try:
|
| 22 |
+
X = np.array(df['feature_vector'].to_list())
|
| 23 |
+
pca_components = 100
|
| 24 |
+
pca = PCA(n_components=pca_components, random_state=42)
|
| 25 |
+
X_reduced = pca.fit_transform(X)
|
| 26 |
+
|
| 27 |
+
kmeans = MiniBatchKMeans(n_clusters=14, random_state=42, batch_size=2048, n_init="auto")
|
| 28 |
+
df['cluster'] = kmeans.fit_predict(X_reduced)
|
| 29 |
+
|
| 30 |
+
df['difficulty_score'] = (df['barre_chords_metric_scaled'] +
|
| 31 |
+
df['major_minor_chords_metric_scaled'] +
|
| 32 |
+
df['special_chords_scaled'] +
|
| 33 |
+
df['song_length_scaled'])
|
| 34 |
+
|
| 35 |
+
cluster_difficulty = df.groupby("cluster")["difficulty_score"].mean().reset_index()
|
| 36 |
+
cluster_difficulty = cluster_difficulty.sort_values("difficulty_score").reset_index(drop=True)
|
| 37 |
+
|
| 38 |
+
difficulty_levels = ["Beginner", "Intermediate", "Advanced"]
|
| 39 |
+
bins = pd.qcut(cluster_difficulty["difficulty_score"], q=len(difficulty_levels), labels=difficulty_levels)
|
| 40 |
+
|
| 41 |
+
cluster_difficulty["difficulty_level"] = bins
|
| 42 |
+
|
| 43 |
+
cluster_map = dict(zip(cluster_difficulty["cluster"], cluster_difficulty["difficulty_level"]))
|
| 44 |
+
df["difficulty_level"] = df["cluster"].map(cluster_map)
|
| 45 |
+
return df
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logger.error("Error in clustering marks dataset : %s", e)
|
| 48 |
+
raise
|
| 49 |
+
|
| 50 |
+
def recommend_songs(exercise_df, prod_df,exercise_id, tempo, genre, top_n=5):
|
| 51 |
+
"""
|
| 52 |
+
Recommend top_n songs similar to the given exercise and tempo.
|
| 53 |
+
Works on PCA-reduced vectors.
|
| 54 |
+
"""
|
| 55 |
+
try:
|
| 56 |
+
exercise_row = exercise_df[
|
| 57 |
+
(exercise_df['exercise_id'] == exercise_id) &
|
| 58 |
+
(exercise_df['tempo'] == tempo)
|
| 59 |
+
]
|
| 60 |
+
if exercise_row.empty:
|
| 61 |
+
raise ValueError("No exercise found with given ID and tempo")
|
| 62 |
+
|
| 63 |
+
exercise_vector = np.array(exercise_row['feature_vector'].iloc[0]).reshape(1, -1)
|
| 64 |
+
filtered_prod_df = prod_df[prod_df['maingenre'] == genre]
|
| 65 |
+
if filtered_prod_df.empty:
|
| 66 |
+
raise ValueError(f"No songs found in genre '{genre}'")
|
| 67 |
+
|
| 68 |
+
similarities = []
|
| 69 |
+
for vec in filtered_prod_df['feature_vector'].values:
|
| 70 |
+
sin = np.dot(exercise_vector, vec) / (np.linalg.norm(exercise_vector) * np.linalg.norm(vec))
|
| 71 |
+
similarities.append(sin)
|
| 72 |
+
|
| 73 |
+
filtered_prod_df = filtered_prod_df.copy()
|
| 74 |
+
filtered_prod_df['similarity'] = similarities
|
| 75 |
+
top_recommendations = filtered_prod_df.sort_values(by='similarity', ascending=False).head(top_n)
|
| 76 |
+
return top_recommendations[['trackname', 'artistnames', 'maingenre', 'chords', 'difficulty_level']]
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error("Error in generating recommendations : %s", e)
|
| 79 |
+
raise
|
| 80 |
+
|
| 81 |
+
def recommend_songs_random(genre,songs_df, recommended_cache, n=5) -> list:
|
| 82 |
+
"""
|
| 83 |
+
Cluster function to retrieve random songs
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
genre (_type_): String value
|
| 87 |
+
n (int, optional): Number of records to retrieve Defaults to 5.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
list: _description_
|
| 91 |
+
"""
|
| 92 |
+
try:
|
| 93 |
+
if songs_df.empty:
|
| 94 |
+
return {"error": "Dataset not loaded"}
|
| 95 |
+
|
| 96 |
+
genre_songs = songs_df[songs_df["maingenre"] == genre]
|
| 97 |
+
|
| 98 |
+
available_songs = genre_songs[~genre_songs["trackname"].isin(recommended_cache)]
|
| 99 |
+
|
| 100 |
+
if available_songs.empty:
|
| 101 |
+
return {"error": f"No new songs available for genre: {genre}"}
|
| 102 |
+
|
| 103 |
+
selected = available_songs.sample(min(n, len(available_songs)), replace=False)
|
| 104 |
+
|
| 105 |
+
recommended_cache.update(selected["trackname"].tolist())
|
| 106 |
+
|
| 107 |
+
return selected[["trackname", "artistnames", "maingenre", "chords",
|
| 108 |
+
"difficulty_level"]].to_dict(orient="records"),recommended_cache
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error("Error retrieving random recommendations: %s", e)
|
| 111 |
+
raise
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 115 |
+
marks_data_file_path = os.path.join(base_dir, '..', 'data', 'processed', 'marks_data.parquet')
|
| 116 |
+
exercise_data_ohe_path = os.path.join(base_dir, '..','data','processed','chord_exercises.parquet')
|
| 117 |
+
marks_df = pd.read_parquet(marks_data_file_path)
|
| 118 |
+
exercise_df = pd.read_parquet(exercise_data_ohe_path)
|
| 119 |
+
df_prod = cluster_function(marks_df)
|
| 120 |
+
df_prod_file_path = os.path.join(base_dir, '..', 'data', 'processed', 'prod_data.parquet')
|
| 121 |
+
df_prod.to_parquet(df_prod_file_path)
|
src/spotify_data_fetch.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import random
|
| 4 |
+
import spotipy
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from spotipy.oauth2 import SpotifyClientCredentials
|
| 7 |
+
from logger import get_logger # pylint: disable=import-error
|
| 8 |
+
|
| 9 |
+
logger = get_logger(__name__)
|
| 10 |
+
|
| 11 |
+
def read_spotify_track_data(track_ids: list,
|
| 12 |
+
output_file: str,
|
| 13 |
+
save_every: int = 5000,
|
| 14 |
+
max_records: int = 25000) -> pd.DataFrame:
|
| 15 |
+
"""
|
| 16 |
+
Fetches Spotify track data in batches with a tiny delay between batches to avoid rate limits.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
track_ids (list): Spotify track ids
|
| 20 |
+
save_every (int): Save checkpoint after this many records
|
| 21 |
+
output_file (str): File path for partial results
|
| 22 |
+
max_records (int): Max number of tracks to fetch in one run
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
pd.DataFrame: Track data (song, artist, id, etc.)
|
| 26 |
+
"""
|
| 27 |
+
try:
|
| 28 |
+
client_id = "754888cc4fa4486daea9cb7917e176fc"
|
| 29 |
+
client_secret = "3f383e012f7442c18851a668b63849dc"
|
| 30 |
+
|
| 31 |
+
sp = spotipy.Spotify(
|
| 32 |
+
auth_manager=SpotifyClientCredentials(
|
| 33 |
+
client_id=client_id,
|
| 34 |
+
client_secret=client_secret
|
| 35 |
+
)
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Load partial results if they exist
|
| 39 |
+
if os.path.exists(output_file):
|
| 40 |
+
existing = pd.read_parquet(output_file)
|
| 41 |
+
processed_ids = set(existing["spotify_song_id"])
|
| 42 |
+
else:
|
| 43 |
+
existing = pd.DataFrame()
|
| 44 |
+
processed_ids = set()
|
| 45 |
+
|
| 46 |
+
# Filter only unprocessed IDs
|
| 47 |
+
remaining_ids = [tid for tid in track_ids if tid not in processed_ids]
|
| 48 |
+
|
| 49 |
+
# Limit to max_records this run
|
| 50 |
+
remaining_ids = remaining_ids[:max_records]
|
| 51 |
+
|
| 52 |
+
results = []
|
| 53 |
+
total = len(remaining_ids)
|
| 54 |
+
|
| 55 |
+
for i in range(0, total, 50):
|
| 56 |
+
batch = remaining_ids[i:i+50]
|
| 57 |
+
response = sp.tracks(batch)
|
| 58 |
+
|
| 59 |
+
for track in response["tracks"]:
|
| 60 |
+
if track is None:
|
| 61 |
+
continue
|
| 62 |
+
track_id = track["id"]
|
| 63 |
+
track_name = track["name"]
|
| 64 |
+
artist_names = ", ".join([artist["name"] for artist in track["artists"]])
|
| 65 |
+
results.append({
|
| 66 |
+
"spotify_song_id": track_id,
|
| 67 |
+
"track_name": track_name,
|
| 68 |
+
"artist_names": artist_names
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
# Tiny random delay to avoid hitting rate limit
|
| 72 |
+
time.sleep(random.uniform(0.2, 0.5))
|
| 73 |
+
|
| 74 |
+
if (i // 50) % (save_every // 50) == 0 and results:
|
| 75 |
+
df_partial = pd.concat([existing, pd.DataFrame(results)], ignore_index=True)
|
| 76 |
+
df_partial.to_parquet(output_file, index=False)
|
| 77 |
+
|
| 78 |
+
# Final save
|
| 79 |
+
final_df = pd.concat([existing, pd.DataFrame(results)], ignore_index=True)
|
| 80 |
+
final_df.to_parquet(output_file, index=False)
|
| 81 |
+
print(f"Run complete. Total saved: {len(final_df)} tracks")
|
| 82 |
+
|
| 83 |
+
return final_df
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.error("Failed fetching spotify data : %s", e)
|
| 86 |
+
raise
|
| 87 |
+
|
| 88 |
+
if __name__ == '__main__':
|
| 89 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 90 |
+
file_path = os.path.join(BASE_DIR, "..", "data", "raw", "songs_data.parquet")
|
| 91 |
+
|
| 92 |
+
chord_data = pd.read_parquet(file_path)
|
| 93 |
+
track_id_list = [str(id) for id in chord_data["spotify_song_id"] if str(id).lower() != 'none']
|
| 94 |
+
output_path = os.path.join(BASE_DIR, "..", "data", "raw", "spotify_tracks.parquet")
|
| 95 |
+
spotify_track_data = read_spotify_track_data(track_id_list, output_path)
|
| 96 |
+
|
| 97 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 98 |
+
spotify_track_data.to_parquet(output_path, index=False)
|
src/utils.py
ADDED
|
File without changes
|
tests/test_utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
from src.main import app # adjust import if needed
|
| 3 |
+
|
| 4 |
+
client = TestClient(app)
|
| 5 |
+
|
| 6 |
+
def test_recommend_random():
|
| 7 |
+
"""Test that /recommend/random runs and returns a response"""
|
| 8 |
+
response = client.get("/recommend/random?genre=rock&n=5")
|
| 9 |
+
assert response.status_code == 200
|
| 10 |
+
assert "recommendations" in response.json()
|
| 11 |
+
|
| 12 |
+
def test_recommend_filtered():
|
| 13 |
+
"""Test that /recommend/filtered runs with tempo + exercise_id"""
|
| 14 |
+
payload = {
|
| 15 |
+
"tempo": 120,
|
| 16 |
+
"exercise_id": 1,
|
| 17 |
+
"genre": "rock"
|
| 18 |
+
}
|
| 19 |
+
response = client.post("/recommend/filtered", json=payload)
|
| 20 |
+
assert response.status_code == 200
|
| 21 |
+
assert "recommendations" in response.json()
|