Upload 17 files
Browse files- .gitattributes +1 -0
- Dockerfile +12 -0
- app.py +12 -0
- app/__init__.py +1 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/api/__init__.py +1 -0
- app/api/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/__pycache__/itunes.cpython-312.pyc +0 -0
- app/api/itunes.py +45 -0
- app/main.py +232 -0
- app/models/__init__.py +1 -0
- app/models/__pycache__/__init__.cpython-312.pyc +0 -0
- app/models/__pycache__/schemas.cpython-312.pyc +0 -0
- app/models/schemas.py +29 -0
- data/processed_songs.csv +3 -0
- data/song_cluster_pipeline.joblib +3 -0
- requirements.txt +29 -0
.gitattributes
CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
server/data/processed_songs.csv filter=lfs diff=lfs merge=lfs -text
|
|
|
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
server/data/processed_songs.csv filter=lfs diff=lfs merge=lfs -text
|
37 |
+
data/processed_songs.csv filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements.txt .
|
6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
7 |
+
|
8 |
+
COPY . .
|
9 |
+
|
10 |
+
EXPOSE 7860
|
11 |
+
|
12 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Create this file in your server/ directory
|
2 |
+
import sys
|
3 |
+
import os
|
4 |
+
|
5 |
+
# Add the current directory to the Python path
|
6 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
7 |
+
|
8 |
+
from app.main import app
|
9 |
+
|
10 |
+
if __name__ == "__main__":
|
11 |
+
import uvicorn
|
12 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
app/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (164 Bytes). View file
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
Binary file (9.32 kB). View file
|
|
app/api/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
app/api/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (168 Bytes). View file
|
|
app/api/__pycache__/itunes.cpython-312.pyc
ADDED
Binary file (2.06 kB). View file
|
|
app/api/itunes.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from fastapi import HTTPException
|
3 |
+
import logging
|
4 |
+
|
5 |
+
logger = logging.getLogger(__name__)
|
6 |
+
|
7 |
+
async def search_itunes_tracks(query: str, limit: int = 1):
|
8 |
+
"""
|
9 |
+
Search iTunes for tracks with 30-second previews
|
10 |
+
"""
|
11 |
+
base_url = "https://itunes.apple.com/search"
|
12 |
+
|
13 |
+
try:
|
14 |
+
params = {
|
15 |
+
"term": query,
|
16 |
+
"entity": "song",
|
17 |
+
"limit": limit
|
18 |
+
}
|
19 |
+
|
20 |
+
response = requests.get(base_url, params=params)
|
21 |
+
response.raise_for_status()
|
22 |
+
|
23 |
+
results = response.json().get('results', [])
|
24 |
+
logger.info(f"Total tracks found: {len(results)}")
|
25 |
+
|
26 |
+
tracks = []
|
27 |
+
for track in results:
|
28 |
+
track_info = {
|
29 |
+
"name": track.get('trackName'),
|
30 |
+
"artist": track.get('artistName'),
|
31 |
+
"preview_url": track.get('previewUrl'),
|
32 |
+
"full_track_url": track.get('trackViewUrl'),
|
33 |
+
"album_image": track.get('artworkUrl100'),
|
34 |
+
"genre": track.get('primaryGenreName'),
|
35 |
+
"album": track.get('collectionName')
|
36 |
+
}
|
37 |
+
|
38 |
+
if track_info['preview_url']:
|
39 |
+
tracks.append(track_info)
|
40 |
+
|
41 |
+
return tracks[0] if tracks else None
|
42 |
+
|
43 |
+
except requests.RequestException as e:
|
44 |
+
logger.error(f"Error searching iTunes: {e}")
|
45 |
+
return None
|
app/main.py
ADDED
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException, Query
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from fastapi.responses import JSONResponse
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
from sklearn.preprocessing import StandardScaler
|
7 |
+
from sklearn.cluster import KMeans
|
8 |
+
import joblib
|
9 |
+
import requests
|
10 |
+
import urllib.parse
|
11 |
+
from typing import List, Optional
|
12 |
+
import os
|
13 |
+
|
14 |
+
app = FastAPI(
|
15 |
+
title="Vibe ML API",
|
16 |
+
description="🎵 AI-powered music recommendation system using K-means clustering",
|
17 |
+
version="1.0.0",
|
18 |
+
docs_url="/docs",
|
19 |
+
redoc_url="/redoc"
|
20 |
+
)
|
21 |
+
|
22 |
+
# Configure CORS for Vercel frontend
|
23 |
+
app.add_middleware(
|
24 |
+
CORSMiddleware,
|
25 |
+
allow_origins=[
|
26 |
+
"http://localhost:3000",
|
27 |
+
"http://localhost:8000",
|
28 |
+
"https://*.vercel.app",
|
29 |
+
"https://your-frontend-url.vercel.app", # Replace with your actual Vercel URL
|
30 |
+
"*" # For development - remove in production
|
31 |
+
],
|
32 |
+
allow_credentials=True,
|
33 |
+
allow_methods=["*"],
|
34 |
+
allow_headers=["*"],
|
35 |
+
)
|
36 |
+
|
37 |
+
# Global variables for ML models and data
|
38 |
+
songs_df = None
|
39 |
+
scaler = None
|
40 |
+
kmeans_model = None
|
41 |
+
|
42 |
+
@app.on_event("startup")
|
43 |
+
async def startup_event():
|
44 |
+
"""Load ML models and data on startup"""
|
45 |
+
global songs_df, scaler, kmeans_model
|
46 |
+
|
47 |
+
try:
|
48 |
+
# Load your data and models here
|
49 |
+
# For now, we'll create a sample dataset
|
50 |
+
# Replace this with your actual data loading logic
|
51 |
+
print("Loading ML models and data...")
|
52 |
+
|
53 |
+
# Sample data structure - replace with your actual data
|
54 |
+
songs_df = pd.DataFrame({
|
55 |
+
'name': ['Blinding Lights', 'Shape of You', 'Bad Guy', 'Levitating'],
|
56 |
+
'artists': [['The Weeknd'], ['Ed Sheeran'], ['Billie Eilish'], ['Dua Lipa']],
|
57 |
+
'year': [2019, 2017, 2019, 2020],
|
58 |
+
'popularity': [95, 94, 93, 92],
|
59 |
+
'danceability': [0.514, 0.825, 0.711, 0.702],
|
60 |
+
'energy': [0.73, 0.652, 0.430, 0.815],
|
61 |
+
'valence': [0.334, 0.931, 0.436, 0.915]
|
62 |
+
})
|
63 |
+
|
64 |
+
# Initialize scaler and kmeans (replace with your trained models)
|
65 |
+
scaler = StandardScaler()
|
66 |
+
kmeans_model = KMeans(n_clusters=20, random_state=42)
|
67 |
+
|
68 |
+
print("✅ Models loaded successfully!")
|
69 |
+
|
70 |
+
except Exception as e:
|
71 |
+
print(f"❌ Error loading models: {e}")
|
72 |
+
|
73 |
+
@app.get("/")
|
74 |
+
async def root():
|
75 |
+
"""Health check endpoint"""
|
76 |
+
return {
|
77 |
+
"message": "🎵 Vibe ML API is running!",
|
78 |
+
"status": "healthy",
|
79 |
+
"version": "1.0.0",
|
80 |
+
"endpoints": {
|
81 |
+
"docs": "/docs",
|
82 |
+
"search": "/search/",
|
83 |
+
"recommendations": "/recommendations/",
|
84 |
+
"song_details": "/song_details/"
|
85 |
+
}
|
86 |
+
}
|
87 |
+
|
88 |
+
@app.get("/search/")
|
89 |
+
async def search_songs(
|
90 |
+
q: str = Query(..., description="Search query (song name or artist)"),
|
91 |
+
limit: int = Query(10, ge=1, le=50, description="Number of results to return")
|
92 |
+
):
|
93 |
+
"""Search for songs by name or artist"""
|
94 |
+
try:
|
95 |
+
if not q or len(q.strip()) < 2:
|
96 |
+
raise HTTPException(status_code=400, detail="Query must be at least 2 characters long")
|
97 |
+
|
98 |
+
# iTunes API search
|
99 |
+
encoded_query = urllib.parse.quote(q)
|
100 |
+
itunes_url = f"https://itunes.apple.com/search?term={encoded_query}&media=music&entity=song&limit={limit}"
|
101 |
+
|
102 |
+
response = requests.get(itunes_url, timeout=10)
|
103 |
+
response.raise_for_status()
|
104 |
+
|
105 |
+
data = response.json()
|
106 |
+
results = data.get('results', [])
|
107 |
+
|
108 |
+
# Format results
|
109 |
+
formatted_results = []
|
110 |
+
for item in results:
|
111 |
+
formatted_results.append({
|
112 |
+
"name": item.get('trackName', 'Unknown'),
|
113 |
+
"artists": [item.get('artistName', 'Unknown Artist')],
|
114 |
+
"year": int(item.get('releaseDate', '2000')[:4]) if item.get('releaseDate') else 2000,
|
115 |
+
"popularity": 50, # Default popularity
|
116 |
+
"preview_info": {
|
117 |
+
"preview_url": item.get('previewUrl'),
|
118 |
+
"album_image": item.get('artworkUrl100', '').replace('100x100', '600x600') if item.get('artworkUrl100') else None
|
119 |
+
}
|
120 |
+
})
|
121 |
+
|
122 |
+
return formatted_results
|
123 |
+
|
124 |
+
except requests.RequestException as e:
|
125 |
+
raise HTTPException(status_code=503, detail=f"External API error: {str(e)}")
|
126 |
+
except Exception as e:
|
127 |
+
raise HTTPException(status_code=500, detail=f"Search error: {str(e)}")
|
128 |
+
|
129 |
+
@app.get("/song_details/")
|
130 |
+
async def get_song_details(
|
131 |
+
song_name: str = Query(..., description="Song name"),
|
132 |
+
artist_name: str = Query(..., description="Artist name")
|
133 |
+
):
|
134 |
+
"""Get detailed information about a specific song"""
|
135 |
+
try:
|
136 |
+
# Search iTunes for the specific song
|
137 |
+
query = f"{song_name} {artist_name}"
|
138 |
+
encoded_query = urllib.parse.quote(query)
|
139 |
+
itunes_url = f"https://itunes.apple.com/search?term={encoded_query}&media=music&entity=song&limit=1"
|
140 |
+
|
141 |
+
response = requests.get(itunes_url, timeout=10)
|
142 |
+
response.raise_for_status()
|
143 |
+
|
144 |
+
data = response.json()
|
145 |
+
results = data.get('results', [])
|
146 |
+
|
147 |
+
if not results:
|
148 |
+
raise HTTPException(status_code=404, detail="Song not found")
|
149 |
+
|
150 |
+
item = results[0]
|
151 |
+
|
152 |
+
song_details = {
|
153 |
+
"name": item.get('trackName', song_name),
|
154 |
+
"artists": [item.get('artistName', artist_name)],
|
155 |
+
"year": int(item.get('releaseDate', '2000')[:4]) if item.get('releaseDate') else 2000,
|
156 |
+
"popularity": 75, # Default popularity
|
157 |
+
"preview_info": {
|
158 |
+
"preview_url": item.get('previewUrl'),
|
159 |
+
"album_image": item.get('artworkUrl100', '').replace('100x100', '600x600') if item.get('artworkUrl100') else None
|
160 |
+
}
|
161 |
+
}
|
162 |
+
|
163 |
+
return song_details
|
164 |
+
|
165 |
+
except requests.RequestException as e:
|
166 |
+
raise HTTPException(status_code=503, detail=f"External API error: {str(e)}")
|
167 |
+
except Exception as e:
|
168 |
+
raise HTTPException(status_code=500, detail=f"Song details error: {str(e)}")
|
169 |
+
|
170 |
+
@app.get("/recommendations/")
|
171 |
+
async def get_recommendations(
|
172 |
+
song_name: str = Query(..., description="Song name for recommendations"),
|
173 |
+
artist_name: str = Query(..., description="Artist name"),
|
174 |
+
number_songs: int = Query(8, ge=1, le=20, description="Number of recommendations")
|
175 |
+
):
|
176 |
+
"""Get AI-powered song recommendations"""
|
177 |
+
try:
|
178 |
+
# For now, return similar songs from iTunes
|
179 |
+
# In production, this would use your ML model
|
180 |
+
|
181 |
+
# Get genre/style from the input song
|
182 |
+
query = f"{song_name} {artist_name}"
|
183 |
+
encoded_query = urllib.parse.quote(query)
|
184 |
+
|
185 |
+
# Search for similar songs
|
186 |
+
similar_query = urllib.parse.quote(artist_name) # Search by artist for similar style
|
187 |
+
itunes_url = f"https://itunes.apple.com/search?term={similar_query}&media=music&entity=song&limit={number_songs + 5}"
|
188 |
+
|
189 |
+
response = requests.get(itunes_url, timeout=10)
|
190 |
+
response.raise_for_status()
|
191 |
+
|
192 |
+
data = response.json()
|
193 |
+
results = data.get('results', [])
|
194 |
+
|
195 |
+
# Filter out the original song and format results
|
196 |
+
recommendations = []
|
197 |
+
for item in results:
|
198 |
+
if item.get('trackName', '').lower() != song_name.lower():
|
199 |
+
recommendations.append({
|
200 |
+
"name": item.get('trackName', 'Unknown'),
|
201 |
+
"artists": [item.get('artistName', 'Unknown Artist')],
|
202 |
+
"year": int(item.get('releaseDate', '2000')[:4]) if item.get('releaseDate') else 2000,
|
203 |
+
"popularity": 70, # Default popularity
|
204 |
+
"preview_info": {
|
205 |
+
"preview_url": item.get('previewUrl'),
|
206 |
+
"album_image": item.get('artworkUrl100', '').replace('100x100', '600x600') if item.get('artworkUrl100') else None
|
207 |
+
}
|
208 |
+
})
|
209 |
+
|
210 |
+
if len(recommendations) >= number_songs:
|
211 |
+
break
|
212 |
+
|
213 |
+
return recommendations[:number_songs]
|
214 |
+
|
215 |
+
except requests.RequestException as e:
|
216 |
+
raise HTTPException(status_code=503, detail=f"External API error: {str(e)}")
|
217 |
+
except Exception as e:
|
218 |
+
raise HTTPException(status_code=500, detail=f"Recommendations error: {str(e)}")
|
219 |
+
|
220 |
+
@app.get("/health")
|
221 |
+
async def health_check():
|
222 |
+
"""Detailed health check"""
|
223 |
+
return {
|
224 |
+
"status": "healthy",
|
225 |
+
"timestamp": "2025-06-19T06:13:28Z",
|
226 |
+
"models_loaded": songs_df is not None,
|
227 |
+
"api_version": "1.0.0"
|
228 |
+
}
|
229 |
+
|
230 |
+
if __name__ == "__main__":
|
231 |
+
import uvicorn
|
232 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
app/models/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
app/models/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (171 Bytes). View file
|
|
app/models/__pycache__/schemas.cpython-312.pyc
ADDED
Binary file (1.58 kB). View file
|
|
app/models/schemas.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel
|
2 |
+
from typing import List, Optional
|
3 |
+
|
4 |
+
class Song(BaseModel):
|
5 |
+
name: str
|
6 |
+
artists: List[str]
|
7 |
+
year: int
|
8 |
+
popularity: int
|
9 |
+
|
10 |
+
class Recommendation(BaseModel):
|
11 |
+
name: str
|
12 |
+
artists: List[str]
|
13 |
+
year: int
|
14 |
+
popularity: int
|
15 |
+
danceability: float
|
16 |
+
energy: float
|
17 |
+
valence: float
|
18 |
+
|
19 |
+
class TrackInfo(BaseModel):
|
20 |
+
name: str
|
21 |
+
artist: str
|
22 |
+
preview_url: Optional[str]
|
23 |
+
full_track_url: Optional[str]
|
24 |
+
album_image: Optional[str]
|
25 |
+
genre: Optional[str]
|
26 |
+
album: Optional[str]
|
27 |
+
|
28 |
+
class RecommendationWithPreview(Recommendation):
|
29 |
+
preview_info: Optional[TrackInfo] = None
|
data/processed_songs.csv
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:358df3704e9d2457b6f1b8383137d877903907a3cf73b5773c95797c93355427
|
3 |
+
size 24572270
|
data/song_cluster_pipeline.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:ceee8795f25c27031dbc76f2a9365dc2d6bbde4dfc51788f4aa089a09f059946
|
3 |
+
size 686654
|
requirements.txt
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.7.0
|
2 |
+
anyio==4.8.0
|
3 |
+
certifi==2024.12.14
|
4 |
+
charset-normalizer==3.4.1
|
5 |
+
click==8.1.8
|
6 |
+
colorama==0.4.6
|
7 |
+
exceptiongroup==1.2.2
|
8 |
+
fastapi==0.115.6
|
9 |
+
h11==0.14.0
|
10 |
+
idna==3.10
|
11 |
+
joblib==1.4.2
|
12 |
+
numpy==2.2.2
|
13 |
+
pandas==2.2.3
|
14 |
+
pydantic==2.10.5
|
15 |
+
pydantic_core==2.27.2
|
16 |
+
python-dateutil==2.9.0.post0
|
17 |
+
python-multipart==0.0.20
|
18 |
+
pytz==2024.2
|
19 |
+
requests==2.32.3
|
20 |
+
scikit-learn==1.6.1
|
21 |
+
scipy==1.15.1
|
22 |
+
six==1.17.0
|
23 |
+
sniffio==1.3.1
|
24 |
+
starlette==0.41.3
|
25 |
+
threadpoolctl==3.5.0
|
26 |
+
typing_extensions==4.12.2
|
27 |
+
tzdata==2024.2
|
28 |
+
urllib3==2.3.0
|
29 |
+
uvicorn==0.34.0
|