BlessedRemzy commited on
Commit
c6f3f01
·
0 Parent(s):

prepare for hugging face spaces deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +15 -0
  2. .gitattributes +2 -0
  3. .gitignore +3 -0
  4. Dockerfile +45 -0
  5. Dockerfile.hf +22 -0
  6. README.md +27 -0
  7. __pycache__/contextual_bandit.cpython-310.pyc +0 -0
  8. __pycache__/contextual_bandit.cpython-311.pyc +0 -0
  9. __pycache__/db_utils.cpython-311.pyc +0 -0
  10. __pycache__/dependencies.cpython-311.pyc +0 -0
  11. __pycache__/emotion_detector.cpython-310.pyc +0 -0
  12. __pycache__/emotion_detector.cpython-311.pyc +0 -0
  13. __pycache__/main.cpython-310.pyc +0 -0
  14. __pycache__/main.cpython-311.pyc +0 -0
  15. __pycache__/recommend_movies.cpython-311.pyc +0 -0
  16. __pycache__/recommend_songs.cpython-311.pyc +0 -0
  17. __pycache__/recommend_songs.cpython-314.pyc +0 -0
  18. __pycache__/routes_movies.cpython-311.pyc +0 -0
  19. __pycache__/routes_recommend.cpython-310.pyc +0 -0
  20. __pycache__/routes_recommend.cpython-311.pyc +0 -0
  21. __pycache__/routes_songs.cpython-311.pyc +0 -0
  22. __pycache__/routes_stress.cpython-310.pyc +0 -0
  23. __pycache__/routes_stress.cpython-311.pyc +0 -0
  24. __pycache__/schemas.cpython-310.pyc +0 -0
  25. __pycache__/schemas.cpython-311.pyc +0 -0
  26. __pycache__/stress_detector.cpython-311.pyc +0 -0
  27. core/__init__.py +19 -0
  28. core/__pycache__/__init__.cpython-311.pyc +0 -0
  29. core/__pycache__/db.cpython-311.pyc +0 -0
  30. core/__pycache__/dependencies.cpython-311.pyc +0 -0
  31. core/__pycache__/schemas.cpython-311.pyc +0 -0
  32. core/db.py +66 -0
  33. core/dependencies.py +58 -0
  34. core/schemas.py +410 -0
  35. explain_approach3.txt +1 -0
  36. explain_new.txt +1 -0
  37. explain_optimized.txt +1 -0
  38. explain_output.txt +1 -0
  39. main.py +318 -0
  40. pixi.lock +0 -0
  41. pixi.toml +33 -0
  42. pyrightconfig.json +11 -0
  43. requirements.txt +12 -0
  44. routes/__init__.py +8 -0
  45. routes/__pycache__/__init__.cpython-311.pyc +0 -0
  46. routes/__pycache__/movies.cpython-311.pyc +0 -0
  47. routes/__pycache__/recommend.cpython-311.pyc +0 -0
  48. routes/__pycache__/recommend.cpython-314.pyc +0 -0
  49. routes/__pycache__/songs.cpython-311.pyc +0 -0
  50. routes/__pycache__/stress.cpython-311.pyc +0 -0
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .pixi
3
+ .git
4
+ .gitignore
5
+ tests/
6
+ *.pyc
7
+ .pytest_cache
8
+ .coverage
9
+ htmlcov/
10
+ *.txt
11
+ *.backup
12
+ pixi.*
13
+ pyrightconfig.json
14
+ ruff.toml
15
+ .gitattributes
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # SCM syntax highlighting & preventing 3-way merges
2
+ pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # pixi environments
2
+ .pixi/*
3
+ !.pixi/config.toml
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ghcr.io/prefix-dev/pixi:0.39.2 AS build
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ build-essential \
5
+ gcc \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # Set working directory
9
+ WORKDIR /app
10
+
11
+ # Copy pixi configuration
12
+ COPY pixi.toml pixi.lock ./
13
+
14
+ # Install dependencies matching the lock file
15
+ RUN pixi install --frozen
16
+
17
+ # Copy source code
18
+ COPY . .
19
+
20
+ # Expose port
21
+ EXPOSE 8000
22
+
23
+ # Set environment variables
24
+ ENV PYTHONUNBUFFERED=1
25
+
26
+ # Create non-root user
27
+ RUN groupadd --gid 1000 appgroup && \
28
+ useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
29
+
30
+ # Create bandit_models directory for persistent bandit data
31
+ RUN mkdir -p bandit_models && chown -R appuser:appgroup bandit_models
32
+
33
+ # Add health check
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
35
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
36
+
37
+ # Set ownership of app directory
38
+ RUN chown -R appuser:appgroup /app
39
+
40
+ # Switch to non-root user
41
+ USER appuser
42
+
43
+ # Run the application
44
+ # We use `pixi run` to ensure we use the environment we just installed
45
+ CMD ["pixi", "run", "start"]
Dockerfile.hf ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ build-essential \
5
+ gcc \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # Create non-root user (HF Spaces security best practice)
9
+ RUN useradd -m -u 1000 user
10
+ USER user
11
+ ENV PATH="/home/user/.local/bin:$PATH"
12
+
13
+ WORKDIR /app
14
+
15
+ COPY --chown=user:user requirements.txt .
16
+ RUN pip install --no-cache-dir --user -r requirements.txt
17
+
18
+ COPY --chown=user:user . .
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Nostalgic Recommendation API
3
+ emoji: 🎬
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ # Nostalgic Recommendation API
12
+
13
+ FastAPI backend for movie and song recommendations using machine learning.
14
+
15
+ ## Features
16
+
17
+ - Movie Recommendations using LightFM collaborative filtering
18
+ - Song Recommendations using content-based filtering with vector similarity
19
+ - Stress and Emotion Detection
20
+ - Contextual Bandit for personalized recommendations
21
+
22
+ ## Endpoints
23
+
24
+ - `GET /health` - Health check and model status
25
+ - `GET /docs` - Interactive API documentation
26
+ - `POST /movies/recommend` - Get movie recommendations
27
+ - `POST /songs/recommend` - Get song recommendations
__pycache__/contextual_bandit.cpython-310.pyc ADDED
Binary file (14.2 kB). View file
 
__pycache__/contextual_bandit.cpython-311.pyc ADDED
Binary file (24.4 kB). View file
 
__pycache__/db_utils.cpython-311.pyc ADDED
Binary file (2.78 kB). View file
 
__pycache__/dependencies.cpython-311.pyc ADDED
Binary file (2.3 kB). View file
 
__pycache__/emotion_detector.cpython-310.pyc ADDED
Binary file (4.92 kB). View file
 
__pycache__/emotion_detector.cpython-311.pyc ADDED
Binary file (7.3 kB). View file
 
__pycache__/main.cpython-310.pyc ADDED
Binary file (7.4 kB). View file
 
__pycache__/main.cpython-311.pyc ADDED
Binary file (14 kB). View file
 
__pycache__/recommend_movies.cpython-311.pyc ADDED
Binary file (20.3 kB). View file
 
__pycache__/recommend_songs.cpython-311.pyc ADDED
Binary file (22.7 kB). View file
 
__pycache__/recommend_songs.cpython-314.pyc ADDED
Binary file (18.7 kB). View file
 
__pycache__/routes_movies.cpython-311.pyc ADDED
Binary file (6.55 kB). View file
 
__pycache__/routes_recommend.cpython-310.pyc ADDED
Binary file (8.54 kB). View file
 
__pycache__/routes_recommend.cpython-311.pyc ADDED
Binary file (17.6 kB). View file
 
__pycache__/routes_songs.cpython-311.pyc ADDED
Binary file (12.6 kB). View file
 
__pycache__/routes_stress.cpython-310.pyc ADDED
Binary file (2.59 kB). View file
 
__pycache__/routes_stress.cpython-311.pyc ADDED
Binary file (4.15 kB). View file
 
__pycache__/schemas.cpython-310.pyc ADDED
Binary file (12.1 kB). View file
 
__pycache__/schemas.cpython-311.pyc ADDED
Binary file (19.7 kB). View file
 
__pycache__/stress_detector.cpython-311.pyc ADDED
Binary file (4.36 kB). View file
 
core/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core package - Utilities, database, and shared components."""
2
+
3
+ from core.db import get_db_connection
4
+ from core.schemas import (
5
+ HealthCheckResponse,
6
+ MovieRecommendRequest,
7
+ MovieRecommendResponse,
8
+ SongRecommendRequest,
9
+ SongRecommendResponse,
10
+ )
11
+
12
+ __all__ = [
13
+ "get_db_connection",
14
+ "HealthCheckResponse",
15
+ "MovieRecommendRequest",
16
+ "MovieRecommendResponse",
17
+ "SongRecommendRequest",
18
+ "SongRecommendResponse",
19
+ ]
core/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (659 Bytes). View file
 
core/__pycache__/db.cpython-311.pyc ADDED
Binary file (2.75 kB). View file
 
core/__pycache__/dependencies.cpython-311.pyc ADDED
Binary file (2.32 kB). View file
 
core/__pycache__/schemas.cpython-311.pyc ADDED
Binary file (20.9 kB). View file
 
core/db.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import psycopg2
3
+ from typing import Dict, Any
4
+ from dotenv import load_dotenv
5
+ from pathlib import Path
6
+
7
+ # Load env (Path: core/ -> fastapi-backend/ -> project_root/)
8
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
9
+ ENV_FILE = PROJECT_ROOT / ".env"
10
+ load_dotenv(ENV_FILE)
11
+
12
+ DATABASE_URL = os.getenv(
13
+ "DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/myapp"
14
+ )
15
+
16
+
17
+ def get_db_connection():
18
+ return psycopg2.connect(DATABASE_URL)
19
+
20
+
21
+ def fetch_latest_context(user_id: str) -> Dict[str, Any]:
22
+ """
23
+ Fetch the latest stress and emotion context for a user from daily_habit_logs.
24
+
25
+ Args:
26
+ user_id: The user's ID
27
+
28
+ Returns:
29
+ Dict with 'stress_score' (float) and 'emotion' (str)
30
+ Defaults to neutral if no log found.
31
+ """
32
+ conn = get_db_connection()
33
+ cursor = conn.cursor()
34
+
35
+ try:
36
+ # Get the most recent log entry for this user
37
+ # We order by date descending to get the latest one
38
+ cursor.execute(
39
+ """
40
+ SELECT stress_level, emotion
41
+ FROM daily_habit_logs
42
+ WHERE user_id = %s
43
+ ORDER BY date DESC, created_at DESC
44
+ LIMIT 1;
45
+ """,
46
+ (user_id,),
47
+ )
48
+
49
+ row = cursor.fetchone()
50
+
51
+ if row:
52
+ stress = row[0]
53
+ emotion = row[1]
54
+ return {
55
+ "stress_score": float(stress) if stress is not None else 0.5,
56
+ "emotion": emotion if emotion else "neutral",
57
+ }
58
+
59
+ except Exception as e:
60
+ print(f"Error fetching latest context: {e}")
61
+ finally:
62
+ cursor.close()
63
+ conn.close()
64
+
65
+ # Default fallback
66
+ return {"stress_score": 0.5, "emotion": "neutral"}
core/dependencies.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dependency injection for FastAPI routes.
3
+
4
+ This module provides dependencies for accessing the loaded recommenders
5
+ via FastAPI's dependency injection system.
6
+ """
7
+
8
+ from typing import TypedDict
9
+
10
+ from fastapi import Request
11
+
12
+ from services.movie_recommender import MovieRecommender
13
+ from services.song_recommender import SongRecommender
14
+
15
+
16
+ class Recommenders(TypedDict):
17
+ """Type definition for the recommenders state."""
18
+
19
+ movie: MovieRecommender | None
20
+ song: SongRecommender | None
21
+
22
+
23
+ def get_movie_recommender(request: Request) -> MovieRecommender:
24
+ """
25
+ Dependency to get the movie recommender from app state.
26
+
27
+ Args:
28
+ request: FastAPI request object
29
+
30
+ Returns:
31
+ MovieRecommender instance
32
+
33
+ Raises:
34
+ RuntimeError: If movie recommender is not loaded
35
+ """
36
+ recommenders: Recommenders = request.app.state.recommenders
37
+ if recommenders["movie"] is None:
38
+ raise RuntimeError("Movie recommender not loaded. Check server startup logs.")
39
+ return recommenders["movie"]
40
+
41
+
42
+ def get_song_recommender(request: Request) -> SongRecommender:
43
+ """
44
+ Dependency to get the song recommender from app state.
45
+
46
+ Args:
47
+ request: FastAPI request object
48
+
49
+ Returns:
50
+ SongRecommender instance
51
+
52
+ Raises:
53
+ RuntimeError: If song recommender is not loaded
54
+ """
55
+ recommenders: Recommenders = request.app.state.recommenders
56
+ if recommenders["song"] is None:
57
+ raise RuntimeError("Song recommender not loaded. Check server startup logs.")
58
+ return recommenders["song"]
core/schemas.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic schemas for request and response validation.
3
+
4
+ This module defines all the data models used for API request/response
5
+ validation and serialization.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ from pydantic import BaseModel, Field, ConfigDict
11
+ from pydantic.alias_generators import to_camel
12
+
13
+
14
+ # =============================================================================
15
+ # Movie Schemas
16
+ # =============================================================================
17
+
18
+
19
+ class MovieBase(BaseModel):
20
+ """Base movie schema with common fields."""
21
+
22
+ movie_id: int = Field(..., description="MovieLens movieId")
23
+ title: str = Field(..., description="Movie title")
24
+ genres: str = Field(
25
+ ..., description="Pipe-separated genres (e.g., 'Action|Comedy')"
26
+ )
27
+
28
+
29
+ class MovieInfo(MovieBase):
30
+ """Full movie information response."""
31
+
32
+ decade: Optional[str] = Field(
33
+ None, description="Decade the movie was released (e.g., '1990s')"
34
+ )
35
+
36
+
37
+ class MovieRecommendation(MovieInfo):
38
+ """Movie recommendation with score."""
39
+
40
+ score: float = Field(..., description="Recommendation score (higher is better)")
41
+
42
+
43
+ class MovieRecommendRequest(BaseModel):
44
+ """Request model for movie recommendations."""
45
+
46
+ liked_movie_ids: list[int] = Field(
47
+ ...,
48
+ min_length=1,
49
+ max_length=50,
50
+ description="List of MovieLens movieIds the user has liked",
51
+ )
52
+ n_recommendations: int = Field(
53
+ default=10,
54
+ ge=1,
55
+ le=100,
56
+ description="Number of recommendations to return",
57
+ )
58
+ exclude_liked: bool = Field(
59
+ default=True,
60
+ description="Whether to exclude liked movies from recommendations",
61
+ )
62
+
63
+
64
+ class MovieRecommendResponse(BaseModel):
65
+ """Response model for movie recommendations."""
66
+
67
+ recommendations: list[MovieRecommendation]
68
+ liked_movies: list[MovieInfo]
69
+
70
+
71
+ class MovieSearchRequest(BaseModel):
72
+ """Request model for movie search."""
73
+
74
+ query: str = Field(..., min_length=1, max_length=200, description="Search query")
75
+ limit: int = Field(
76
+ default=10, ge=1, le=100, description="Maximum number of results"
77
+ )
78
+
79
+
80
+ class MovieSearchResponse(BaseModel):
81
+ """Response model for movie search."""
82
+
83
+ results: list[MovieInfo]
84
+ query: str
85
+
86
+
87
+ # =============================================================================
88
+ # Song Schemas
89
+ # =============================================================================
90
+
91
+
92
+ class SongBase(BaseModel):
93
+ """Base song schema with common fields."""
94
+
95
+ spotify_id: str = Field(..., description="Spotify track ID")
96
+ name: str = Field(..., description="Song name")
97
+ artists: str = Field(..., description="Artist name(s)")
98
+
99
+
100
+ class SongInfo(SongBase):
101
+ """Full song information response."""
102
+
103
+ genre: Optional[str] = Field(None, description="Genre of the song")
104
+ year: Optional[int] = Field(None, description="Release year")
105
+
106
+
107
+ class SongDetails(SongInfo):
108
+ """Extended song information with audio features."""
109
+
110
+ danceability: Optional[float] = Field(
111
+ None, ge=0, le=1, description="Danceability score"
112
+ )
113
+ energy: Optional[float] = Field(None, ge=0, le=1, description="Energy score")
114
+ key: Optional[int] = Field(None, ge=0, le=11, description="Musical key (0-11)")
115
+ loudness: Optional[float] = Field(None, description="Loudness in dB")
116
+ mode: Optional[int] = Field(None, ge=0, le=1, description="Mode (0=minor, 1=major)")
117
+ speechiness: Optional[float] = Field(
118
+ None, ge=0, le=1, description="Speechiness score"
119
+ )
120
+ acousticness: Optional[float] = Field(
121
+ None, ge=0, le=1, description="Acousticness score"
122
+ )
123
+ instrumentalness: Optional[float] = Field(
124
+ None, ge=0, le=1, description="Instrumentalness score"
125
+ )
126
+ liveness: Optional[float] = Field(None, ge=0, le=1, description="Liveness score")
127
+ valence: Optional[float] = Field(
128
+ None, ge=0, le=1, description="Valence (happiness) score"
129
+ )
130
+ tempo: Optional[float] = Field(None, ge=0, description="Tempo in BPM")
131
+ niche_genres: Optional[str] = Field(None, description="Niche genre tags")
132
+
133
+
134
+ class SongRecommendation(SongInfo):
135
+ """Song recommendation with similarity score."""
136
+
137
+ similarity: float = Field(
138
+ ..., ge=0, le=1, description="Cosine similarity score (0-1)"
139
+ )
140
+
141
+
142
+ class SongRecommendRequest(BaseModel):
143
+ """Request model for song recommendations based on liked songs."""
144
+
145
+ liked_song_ids: list[str] = Field(
146
+ ...,
147
+ min_length=1,
148
+ max_length=50,
149
+ description="List of Spotify track IDs the user likes",
150
+ )
151
+ n_recommendations: int = Field(
152
+ default=10,
153
+ ge=1,
154
+ le=100,
155
+ description="Number of recommendations to return",
156
+ )
157
+ exclude_liked: bool = Field(
158
+ default=True,
159
+ description="Whether to exclude liked songs from recommendations",
160
+ )
161
+
162
+
163
+ class SongRecommendByIdRequest(BaseModel):
164
+ """Request model for song recommendations based on a single song."""
165
+
166
+ spotify_id: str = Field(..., description="Spotify track ID")
167
+ n_recommendations: int = Field(
168
+ default=10,
169
+ ge=1,
170
+ le=100,
171
+ description="Number of recommendations to return",
172
+ )
173
+
174
+
175
+ class SongRecommendResponse(BaseModel):
176
+ """Response model for song recommendations."""
177
+
178
+ recommendations: list[SongRecommendation]
179
+ query_songs: list[SongInfo]
180
+
181
+
182
+ class SongSearchRequest(BaseModel):
183
+ """Request model for song search."""
184
+
185
+ query: str = Field(
186
+ ...,
187
+ min_length=1,
188
+ max_length=200,
189
+ description="Search query for song name or artist",
190
+ )
191
+ limit: int = Field(
192
+ default=10, ge=1, le=100, description="Maximum number of results"
193
+ )
194
+
195
+
196
+ class SongSearchResponse(BaseModel):
197
+ """Response model for song search."""
198
+
199
+ results: list[SongInfo]
200
+ query: str
201
+
202
+
203
+ # =============================================================================
204
+ # Text Analysis Schemas (Stress + Emotion Detection)
205
+ # =============================================================================
206
+
207
+
208
+ class TextAnalysisRequest(BaseModel):
209
+ """Request model for text analysis (stress and emotion detection)."""
210
+
211
+ text: str = Field(
212
+ ...,
213
+ min_length=1,
214
+ max_length=5000,
215
+ description="Text to analyze for stress and emotion",
216
+ )
217
+
218
+
219
+ class EmotionResult(BaseModel):
220
+ """Emotion detection result."""
221
+
222
+ emotion: str = Field(
223
+ ...,
224
+ description="Predicted emotion (anger, fear, joy, love, neutral, sadness, surprise)",
225
+ )
226
+ confidence: float = Field(
227
+ ...,
228
+ ge=0,
229
+ le=1,
230
+ description="Confidence score for the prediction",
231
+ )
232
+ probabilities: dict[str, float] = Field(
233
+ ...,
234
+ description="Probability distribution across all emotions",
235
+ )
236
+
237
+
238
+ class TextAnalysisResponse(BaseModel):
239
+ """Response model for text analysis."""
240
+
241
+ text: str = Field(..., description="The analyzed text")
242
+ stress_score: float = Field(
243
+ ...,
244
+ ge=0,
245
+ le=1,
246
+ description="Stress level (0=no stress, 1=high stress)",
247
+ )
248
+ emotion: EmotionResult = Field(..., description="Detected emotion")
249
+
250
+
251
+ class AnalyzeRequest(BaseModel):
252
+ """Request model for text analysis."""
253
+
254
+ text: str = Field(..., min_length=1, max_length=5000, description="Text to analyze")
255
+
256
+
257
+ class AnalyzeResponse(BaseModel):
258
+ """Response model for text analysis."""
259
+
260
+ stress_score: float = Field(..., ge=0, description="Stress level")
261
+ emotion: EmotionResult = Field(..., description="Detected emotion")
262
+
263
+
264
+ # Legacy schemas for backwards compatibility
265
+ class StressDetectionRequest(BaseModel):
266
+ """Request model for stress detection (legacy)."""
267
+
268
+ text: str = Field(
269
+ ...,
270
+ min_length=1,
271
+ max_length=5000,
272
+ description="Text to analyze for stress",
273
+ )
274
+
275
+
276
+ class StressDetectionResponse(BaseModel):
277
+ """Response model for stress detection (legacy)."""
278
+
279
+ stress_score: float = Field(
280
+ ...,
281
+ ge=0,
282
+ le=1,
283
+ description="Stress level (0=no stress, 1=high stress)",
284
+ )
285
+ text: str = Field(..., description="The analyzed text")
286
+
287
+
288
+ # =============================================================================
289
+ # Health Check Schema
290
+ # =============================================================================
291
+
292
+
293
+ class HealthCheckResponse(BaseModel):
294
+ """Health check response."""
295
+
296
+ status: str = Field(..., description="API status")
297
+ movie_model_loaded: bool = Field(..., description="Whether movie model is loaded")
298
+ song_model_loaded: bool = Field(..., description="Whether song model is loaded")
299
+ stress_model_loaded: bool = Field(..., description="Whether stress model is loaded")
300
+ emotion_model_loaded: bool = Field(
301
+ ..., description="Whether emotion model is loaded"
302
+ )
303
+ bandit_loaded: bool = Field(..., description="Whether bandit model is loaded")
304
+ version: str = Field(..., description="API version")
305
+
306
+
307
+ # =============================================================================
308
+ # Unified Recommendation Schemas
309
+ # =============================================================================
310
+
311
+
312
+ class RecommendRequest(BaseModel):
313
+ """Request model for unified recommendation endpoint."""
314
+
315
+ user_id: str = Field(..., description="User ID")
316
+ journal_text: str = Field(
317
+ default="",
318
+ max_length=5000,
319
+ description="Journal text to analyze for stress and emotion",
320
+ )
321
+
322
+
323
+ class RecommendedContent(BaseModel):
324
+ """Recommended content (either song or movie)."""
325
+
326
+ type: str = Field(..., description="Content type: 'song' or 'movie'")
327
+ id: str = Field(
328
+ ..., description="Content ID (spotify_id for songs, movieId for movies)"
329
+ )
330
+
331
+ # Movie fields
332
+ title: Optional[str] = Field(None, description="Movie title")
333
+ genres: Optional[list[str]] = Field(None, description="Movie genres")
334
+
335
+ # Song fields
336
+ name: Optional[str] = Field(None, description="Song name")
337
+ artists: Optional[list[str]] = Field(None, description="Song artists")
338
+ genre: Optional[str] = Field(None, description="Song genre")
339
+
340
+ # Common fields
341
+ year: Optional[int] = Field(None, description="Release year")
342
+
343
+
344
+ class RecommendResponse(BaseModel):
345
+ """Response model for unified recommendation endpoint."""
346
+
347
+ content: RecommendedContent = Field(..., description="Recommended content")
348
+ stress_score: float = Field(..., ge=0, le=1, description="Detected stress level")
349
+ emotion: EmotionResult = Field(..., description="Detected emotion")
350
+ bandit_score: float = Field(..., ge=0, description="Bandit confidence score")
351
+
352
+
353
+ class RecommendFeedbackRequest(BaseModel):
354
+ """Request model for recommendation feedback."""
355
+
356
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
357
+
358
+ user_id: str = Field(..., description="User ID")
359
+ content_type: str = Field(..., description="Content type: 'song' or 'movie'")
360
+ content_id: str = Field(..., description="Content ID")
361
+
362
+ # Interaction fields
363
+ interaction_type: str = Field(
364
+ default="feedback",
365
+ description="Type of interaction (feedback, view, click, skip, next, replay)",
366
+ )
367
+ duration_seconds: Optional[int] = Field(
368
+ None, description="Duration of interaction in seconds"
369
+ )
370
+ feedback_submitted: bool = Field(
371
+ default=False,
372
+ description="Whether explicit feedback has already been submitted for this session",
373
+ )
374
+
375
+ # Primary feedback signal (optional now, as implicit interactions might not have it)
376
+ brings_back_memories: Optional[bool] = Field(
377
+ None, description="Primary feedback signal"
378
+ )
379
+
380
+ # Content metadata for bandit update
381
+ content_year: Optional[int] = Field(None, description="Content year")
382
+ content_genre: Optional[str] = Field(None, description="Content genre")
383
+
384
+ # Context snapshot (state at time of recommendation)
385
+ context_stress: Optional[float] = Field(
386
+ None, description="Stress score when recommendation was made"
387
+ )
388
+ context_emotion: Optional[str] = Field(
389
+ None, description="Emotion when recommendation was made"
390
+ )
391
+
392
+
393
+ class RecommendFeedbackResponse(BaseModel):
394
+ """Response model for recommendation feedback."""
395
+
396
+ success: bool = Field(..., description="Whether feedback was recorded")
397
+ reward: float = Field(..., ge=0, le=1, description="Calculated reward value")
398
+ message: str = Field(..., description="Status message")
399
+
400
+
401
+ # =============================================================================
402
+ # Error Schemas
403
+ # =============================================================================
404
+
405
+
406
+ class ErrorResponse(BaseModel):
407
+ """Error response model."""
408
+
409
+ detail: str = Field(..., description="Error message")
410
+ error_type: str = Field(default="error", description="Type of error")
explain_approach3.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ planning time: 5msexecution time: 292msdistribution: localvectorized: trueplan type: customrows decoded from KV: 1,525 (720 KiB, 2 gRPC calls)cumulative time spent in KV: 207msmaximum memory usage: 1.3 MiBDistSQL network usage: 0 B (0 messages)sql cpu time: 62msisolation level: serializablepriority: normalquality of service: regular• top-k│ sql nodes: n1│ actual row count: 10│ execution time: 87µs│ estimated max memory allocated: 10 KiB│ sql cpu time: 87µs│ estimated row count: 1│ order: -similarity│ k: 10│└── • render │ └── • filter │ sql nodes: n1 │ actual row count: 159 │ execution time: 3ms │ sql cpu time: 3ms │ estimated row count: 1 │ filter: row_number = 1 │ └── • window │ sql nodes: n1 │ actual row count: 165 │ execution time: 480µs │ estimated max memory allocated: 89 KiB │ sql cpu time: 478µs │ estimated row count: 500 │ └── • render │ └── • lookup join │ sql nodes: n1 │ kv nodes: n1 │ actual row count: 165 │ KV time: 106ms │ KV rows decoded: 500 │ KV bytes read: 135 KiB │ KV gRPC calls: 1 │ execution time: 111ms │ estimated max memory allocated: 200 KiB │ sql cpu time: 4ms │ estimated row count: 500 │ table: songs@songs_pkey │ equality: (spotify_id) = (id) │ equality cols are key │ pred: year <= 2016 │ └── • top-k │ sql nodes: n1 │ actual row count: 500 │ execution time: 4ms │ estimated max memory allocated: 50 KiB │ sql cpu time: 4ms │ estimated row count: 500 │ order: +distance │ k: 500 │ └── • render │ └── • lookup join │ sql nodes: n1 │ kv nodes: n1 │ actual row count: 1,025 │ KV time: 101ms │ KV rows decoded: 1,025 │ KV bytes read: 584 KiB │ KV gRPC calls: 1 │ execution time: 110ms │ estimated max memory allocated: 280 KiB │ sql cpu time: 9ms │ table: song_vectors@song_vectors_pkey │ equality: (id) = (id) │ equality cols are key │ └── • vector search sql nodes: n1 actual row count: 1,025 execution time: 55ms sql cpu time: 38ms table: song_vectors@song_vectors_embedding_idx target count: 500
explain_new.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ planning time: 2msexecution time: 8.3sdistribution: localvectorized: trueplan type: customrows decoded from KV: 1,101,238 (464 MiB, 47 gRPC calls)cumulative time spent in KV: 2.7smaximum memory usage: 120 MiBDistSQL network usage: 0 B (0 messages)max sql temp disk usage: 80 MiBsql cpu time: 5.9sisolation level: serializablepriority: normalquality of service: regular• top-k│ sql nodes: n1│ actual row count: 10│ execution time: 32µs│ estimated max memory allocated: 10 KiB│ sql cpu time: 32µs│ estimated row count: 1│ order: -similarity│ k: 10│└── • render │ └── • filter │ sql nodes: n1 │ actual row count: 475 │ execution time: 6µs │ sql cpu time: 7µs │ estimated row count: 1 │ filter: row_number = 1 │ └── • top-k │ sql nodes: n1 │ actual row count: 500 │ execution time: 8ms │ estimated max memory allocated: 170 KiB │ sql cpu time: 8ms │ estimated row count: 500 │ order: +column38 │ k: 500 │ └── • render │ └── • window │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 2.1s │ estimated max memory allocated: 55 MiB │ sql cpu time: 1.8s │ estimated row count: 368,267 │ └── • render │ └── • hash join │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 2.7s │ estimated max memory allocated: 77 MiB │ sql cpu time: 2.3s │ estimated row count: 368,267 │ equality: (spotify_id) = (id) │ left cols are key │ right cols are key │ ├── • scan │ sql nodes: n1 │ kv nodes: n1 │ actual row count: 550,619 │ KV time: 1.8s │ KV rows decoded: 550,619 │ KV bytes read: 317 MiB │ KV gRPC calls: 32 │ estimated max memory allocated: 11 MiB │ sql cpu time: 720ms │ estimated row count: 574,254 (100% of the table; stats collected 1 hour ago; using stats forecast for 1 hour ago) │ table: song_vectors@song_vectors_pkey │ spans: FULL SCAN │ └── • filter │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 3ms │ sql cpu time: 3ms │ estimated row count: 368,267 │ filter: year <= 2016 │ └── • scan sql nodes: n1 kv nodes: n1 actual row count: 550,619 KV time: 927ms KV rows decoded: 550,619 KV bytes read: 147 MiB KV gRPC calls: 15 estimated max memory allocated: 10 MiB sql cpu time: 218ms estimated row count: 505,005 (100% of the table; stats collected 2 hours ago) table: songs@songs_pkey spans: FULL SCAN
explain_optimized.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ planning time: 2msexecution time: 7.2sdistribution: localvectorized: trueplan type: customrows decoded from KV: 1,101,238 (464 MiB, 47 gRPC calls)cumulative time spent in KV: 2.1smaximum memory usage: 121 MiBDistSQL network usage: 0 B (0 messages)max sql temp disk usage: 80 MiBsql cpu time: 5.4sisolation level: serializablepriority: normalquality of service: regular• top-k│ sql nodes: n1│ actual row count: 10│ execution time: 23µs│ estimated max memory allocated: 10 KiB│ sql cpu time: 23µs│ estimated row count: 1│ order: -similarity│ k: 10│└── • render │ └── • filter │ sql nodes: n1 │ actual row count: 475 │ execution time: 5µs │ sql cpu time: 5µs │ estimated row count: 1 │ filter: row_number = 1 │ └── • top-k │ sql nodes: n1 │ actual row count: 500 │ execution time: 9ms │ estimated max memory allocated: 170 KiB │ sql cpu time: 9ms │ estimated row count: 500 │ order: +column38 │ k: 500 │ └── • render │ └── • window │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 2.1s │ estimated max memory allocated: 55 MiB │ sql cpu time: 1.8s │ estimated row count: 368,267 │ └── • render │ └── • hash join │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 2.3s │ estimated max memory allocated: 77 MiB │ sql cpu time: 1.9s │ estimated row count: 368,267 │ equality: (spotify_id) = (id) │ left cols are key │ right cols are key │ ├── • scan │ sql nodes: n1 │ kv nodes: n1 │ actual row count: 550,619 │ KV time: 1.3s │ KV rows decoded: 550,619 │ KV bytes read: 317 MiB │ KV gRPC calls: 32 │ estimated max memory allocated: 11 MiB │ sql cpu time: 630ms │ estimated row count: 574,254 (100% of the table; stats collected 1 hour ago; using stats forecast for 1 hour ago) │ table: song_vectors@song_vectors_pkey │ spans: FULL SCAN │ └── • filter │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 3ms │ sql cpu time: 3ms │ estimated row count: 368,267 │ filter: year <= 2016 │ └── • scan sql nodes: n1 kv nodes: n1 actual row count: 550,619 KV time: 777ms KV rows decoded: 550,619 KV bytes read: 147 MiB KV gRPC calls: 15 estimated max memory allocated: 10 MiB sql cpu time: 237ms estimated row count: 505,005 (100% of the table; stats collected 2 hours ago) table: songs@songs_pkey spans: FULL SCAN
explain_output.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ planning time: 194µsexecution time: 4.4sdistribution: localvectorized: trueplan type: generic, reusedrows decoded from KV: 1,101,238 (464 MiB, 47 gRPC calls)cumulative time spent in KV: 1.5smaximum memory usage: 122 MiBDistSQL network usage: 0 B (0 messages)max sql temp disk usage: 74 MiBsql cpu time: 3.2sisolation level: serializablepriority: normalquality of service: regular• render│└── • limit │ count: 5 │ └── • filter │ sql nodes: n1 │ actual row count: 5 │ execution time: 10µs │ sql cpu time: 10µs │ estimated row count: 1 │ filter: row_number = 1 │ └── • window │ sql nodes: n1 │ actual row count: 1,024 │ execution time: 359ms │ estimated max memory allocated: 71 MiB │ sql cpu time: 301ms │ estimated row count: 368,267 │ └── • render │ └── • hash join │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 1.9s │ estimated max memory allocated: 77 MiB │ sql cpu time: 1.6s │ estimated row count: 368,267 │ equality: (spotify_id) = (id) │ left cols are key │ right cols are key │ ├── • scan │ sql nodes: n1 │ kv nodes: n1 │ actual row count: 550,619 │ KV time: 1s │ KV rows decoded: 550,619 │ KV bytes read: 317 MiB │ KV gRPC calls: 32 │ estimated max memory allocated: 11 MiB │ sql cpu time: 583ms │ estimated row count: 574,254 (100% of the table; stats collected 1 hour ago; using stats forecast for 59 minutes ago) │ table: song_vectors@song_vectors_pkey │ spans: FULL SCAN │ └── • filter │ sql nodes: n1 │ actual row count: 400,927 │ execution time: 2ms │ sql cpu time: 2ms │ estimated row count: 368,267 │ filter: year <= 2016 │ └── • scan sql nodes: n1 kv nodes: n1 actual row count: 550,619 KV time: 491ms KV rows decoded: 550,619 KV bytes read: 147 MiB KV gRPC calls: 15 estimated max memory allocated: 10 MiB sql cpu time: 204ms estimated row count: 505,005 (100% of the table; stats collected 2 hours ago) table: songs@songs_pkey spans: FULL SCAN
main.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI Recommendation Server
2
+
3
+ This server provides endpoints for movie and song recommendations.
4
+ Models are loaded on startup using FastAPI's lifespan events.
5
+
6
+ Run with: uvicorn main:app --reload
7
+ """
8
+
9
+ import os
10
+ import traceback
11
+ from contextlib import asynccontextmanager
12
+ from pathlib import Path
13
+ from typing import AsyncGenerator
14
+
15
+ from dotenv import load_dotenv
16
+ from fastapi import FastAPI
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+
19
+ ENV_FILE = Path(__file__).parent.parent / ".env"
20
+ load_dotenv(ENV_FILE)
21
+
22
+ from routes.movies import router as movies_router # noqa: E402
23
+ from routes.songs import router as songs_router # noqa: E402
24
+ from routes.stress import router as analyze_router # noqa: E402
25
+ from routes.recommend import router as recommend_router # noqa: E402
26
+ from core.schemas import HealthCheckResponse # noqa: E402
27
+ from services.movie_recommender import MovieRecommender # noqa: E402
28
+ from services.song_recommender import SongRecommender # noqa: E402
29
+ from services.stress_detector import StressDetector # noqa: E402
30
+ from services.emotion_detector import EmotionDetector # noqa: E402
31
+ from services.contextual_bandit import HierarchicalBandit # noqa: E402
32
+
33
+
34
+ # =============================================================================
35
+ # Configuration
36
+ # =============================================================================
37
+
38
+ API_VERSION = "1.0.0"
39
+ API_TITLE = "Nostalgic Recommendation API"
40
+ API_DESCRIPTION = """
41
+ A recommendation API for movies and songs using machine learning models.
42
+
43
+ ## Features
44
+
45
+ - 🎬 **Movie Recommendations**: Using LightFM collaborative filtering with cold-start support
46
+ - 🎵 **Song Recommendations**: Using content-based filtering with pgvector similarity search
47
+ - 🔍 **Search**: Search movies by title, songs by name or artist
48
+ - ✨ **Validation**: Full request/response validation with Pydantic
49
+
50
+ ## Models
51
+
52
+ - **Movie Recommender**: LightFM model trained on MovieLens 32M dataset
53
+ - **Song Recommender**: Content-based filtering using audio features, stored in pgvector
54
+ """
55
+
56
+ # CORS configuration
57
+ CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",")
58
+
59
+
60
+ # =============================================================================
61
+ # Model State
62
+ # =============================================================================
63
+
64
+
65
+ class AppState:
66
+ """Application state container for recommenders."""
67
+
68
+ movie_recommender: MovieRecommender | None = None
69
+ song_recommender: SongRecommender | None = None
70
+ movie_model_loaded: bool = False
71
+ song_model_loaded: bool = False
72
+
73
+
74
+ # =============================================================================
75
+ # Lifespan Events
76
+ # =============================================================================
77
+
78
+
79
+ @asynccontextmanager
80
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
81
+ """
82
+ Manage application lifespan.
83
+
84
+ On startup: Load movie and song recommendation models.
85
+ On shutdown: Clean up resources.
86
+ """
87
+ print("=" * 60)
88
+ print(f"🚀 Starting {API_TITLE} v{API_VERSION}")
89
+ print("=" * 60)
90
+
91
+ # Initialize state
92
+ app.state.recommenders = {
93
+ "movie": None,
94
+ "song": None,
95
+ "stress": None,
96
+ "emotion": None,
97
+ "bandit": None,
98
+ }
99
+ app.state.model_status = {
100
+ "movie_loaded": False,
101
+ "song_loaded": False,
102
+ "stress_loaded": False,
103
+ "emotion_loaded": False,
104
+ "bandit_loaded": False,
105
+ }
106
+
107
+ # Load movie recommender
108
+ print("\n📽️ Loading Movie Recommender...")
109
+ try:
110
+ movie_recommender = MovieRecommender()
111
+ app.state.recommenders["movie"] = movie_recommender # type: ignore[assignment]
112
+ app.state.model_status["movie_loaded"] = True # type: ignore[assignment]
113
+ print("✅ Movie Recommender loaded successfully!")
114
+ except FileNotFoundError as e:
115
+ print(f"⚠️ Movie model files not found: {e}")
116
+ print(" Movie recommendations will be unavailable.")
117
+ except Exception as e:
118
+ print(f"❌ Error loading Movie Recommender: {e}")
119
+ traceback.print_exc()
120
+
121
+ # Load song recommender
122
+ print("\n🎵 Loading Song Recommender...")
123
+ try:
124
+ song_recommender = SongRecommender()
125
+ app.state.recommenders["song"] = song_recommender # type: ignore[assignment]
126
+ app.state.model_status["song_loaded"] = True # type: ignore[assignment]
127
+ print("✅ Song Recommender loaded successfully!")
128
+ except FileNotFoundError as e:
129
+ print(f"⚠️ Song model files not found: {e}")
130
+ print(" Song recommendations will be unavailable.")
131
+ except Exception as e:
132
+ print(f"❌ Error loading Song Recommender: {e}")
133
+ print(" This may be due to database connection issues.")
134
+ traceback.print_exc()
135
+
136
+ # Load stress detector
137
+ print("\n🧠 Loading Stress Detector...")
138
+ try:
139
+ stress_detector = StressDetector()
140
+ app.state.recommenders["stress"] = stress_detector # type: ignore[assignment]
141
+ app.state.model_status["stress_loaded"] = True # type: ignore[assignment]
142
+ print("✅ Stress Detector loaded successfully!")
143
+ except FileNotFoundError as e:
144
+ print(f"⚠️ Stress model files not found: {e}")
145
+ print(" Stress detection will be unavailable.")
146
+ except Exception as e:
147
+ print(f"❌ Error loading Stress Detector: {e}")
148
+ traceback.print_exc()
149
+
150
+ # Load emotion detector
151
+ print("\n💭 Loading Emotion Detector...")
152
+ try:
153
+ emotion_detector = EmotionDetector(use_mock=False)
154
+ app.state.recommenders["emotion"] = emotion_detector # type: ignore[assignment]
155
+ app.state.model_status["emotion_loaded"] = True # type: ignore[assignment]
156
+ print("✅ Emotion Detector loaded successfully!")
157
+ except Exception as e:
158
+ print(f"⚠️ Error loading real Emotion Detector: {e}")
159
+ print(" Falling back to MOCK Emotion Detector.")
160
+ try:
161
+ emotion_detector = EmotionDetector(use_mock=True)
162
+ app.state.recommenders["emotion"] = emotion_detector # type: ignore[assignment]
163
+ app.state.model_status["emotion_loaded"] = True # type: ignore[assignment]
164
+ print("✅ Mock Emotion Detector loaded.")
165
+ except Exception as e2:
166
+ print(f"❌ Error loading Mock Emotion Detector: {e2}")
167
+ traceback.print_exc()
168
+
169
+ # Load contextual bandit
170
+ print("\n🎰 Loading Contextual Bandit...")
171
+ try:
172
+ bandit = HierarchicalBandit()
173
+ app.state.recommenders["bandit"] = bandit # type: ignore[assignment]
174
+ app.state.model_status["bandit_loaded"] = True # type: ignore[assignment]
175
+ print("✅ Contextual Bandit loaded successfully!")
176
+ except Exception as e:
177
+ print(f"❌ Error loading Contextual Bandit: {e}")
178
+ traceback.print_exc()
179
+
180
+ print("\n" + "=" * 60)
181
+ print("🌐 Server is ready to accept requests")
182
+ print("=" * 60 + "\n")
183
+
184
+ # Yield control to the application
185
+ yield
186
+
187
+ # Cleanup on shutdown
188
+ print("\n🛑 Shutting down server...")
189
+
190
+ # Close database connections
191
+ if app.state.recommenders["movie"]:
192
+ app.state.recommenders["movie"].close()
193
+ print(" Closed movie recommender database connection.")
194
+
195
+ if app.state.recommenders["song"]:
196
+ app.state.recommenders["song"].close()
197
+ print(" Closed song recommender database connection.")
198
+
199
+ if app.state.recommenders["stress"]:
200
+ app.state.recommenders["stress"].close()
201
+ print(" Closed stress detector.")
202
+
203
+ if app.state.recommenders["emotion"]:
204
+ app.state.recommenders["emotion"].close()
205
+ print(" Closed emotion detector.")
206
+
207
+ if app.state.recommenders["bandit"]:
208
+ app.state.recommenders["bandit"].close()
209
+ print(" Closed contextual bandit.")
210
+
211
+ # Clear references
212
+ app.state.recommenders["movie"] = None
213
+ app.state.recommenders["song"] = None
214
+ app.state.recommenders["stress"] = None
215
+ app.state.recommenders["emotion"] = None
216
+ app.state.recommenders["bandit"] = None
217
+
218
+ print("👋 Server shutdown complete.\n")
219
+
220
+
221
+ # =============================================================================
222
+ # FastAPI Application
223
+ # =============================================================================
224
+
225
+ app = FastAPI(
226
+ title=API_TITLE,
227
+ description=API_DESCRIPTION,
228
+ version=API_VERSION,
229
+ lifespan=lifespan,
230
+ docs_url="/docs",
231
+ redoc_url="/redoc",
232
+ openapi_url="/openapi.json",
233
+ )
234
+
235
+
236
+ # Add CORS middleware
237
+ app.add_middleware(
238
+ CORSMiddleware,
239
+ allow_origins=CORS_ORIGINS,
240
+ allow_credentials=True,
241
+ allow_methods=["*"],
242
+ allow_headers=["*"],
243
+ )
244
+
245
+
246
+ # =============================================================================
247
+ # Health Check Endpoint
248
+ # =============================================================================
249
+
250
+
251
+ @app.get(
252
+ "/health",
253
+ response_model=HealthCheckResponse,
254
+ tags=["Health"],
255
+ summary="Health check",
256
+ description="Check the API status and model availability.",
257
+ )
258
+ async def health_check() -> HealthCheckResponse:
259
+ """Return the API health status and model loading status."""
260
+ return HealthCheckResponse(
261
+ status="healthy",
262
+ movie_model_loaded=app.state.model_status.get("movie_loaded", False),
263
+ song_model_loaded=app.state.model_status.get("song_loaded", False),
264
+ stress_model_loaded=app.state.model_status.get("stress_loaded", False),
265
+ emotion_model_loaded=app.state.model_status.get("emotion_loaded", False),
266
+ bandit_loaded=app.state.model_status.get("bandit_loaded", False),
267
+ version=API_VERSION,
268
+ )
269
+
270
+
271
+ @app.get("/", tags=["Root"])
272
+ async def root() -> dict[str, str]:
273
+ """Root endpoint with welcome message."""
274
+ return {
275
+ "message": f"Welcome to {API_TITLE}",
276
+ "version": API_VERSION,
277
+ "docs": "/docs",
278
+ "health": "/health",
279
+ }
280
+
281
+
282
+ # =============================================================================
283
+ # Register Routers
284
+ # =============================================================================
285
+
286
+ app.include_router(movies_router)
287
+ app.include_router(songs_router)
288
+ app.include_router(analyze_router)
289
+ app.include_router(recommend_router)
290
+
291
+
292
+ # =============================================================================
293
+ # Entry Point
294
+ # =============================================================================
295
+
296
+ if __name__ == "__main__":
297
+ import signal
298
+ import sys
299
+ import uvicorn
300
+
301
+ def shutdown_handler(signum, frame):
302
+ print(f"\n🛑 Received signal {signum}, initiating graceful shutdown...")
303
+ sys.exit(0)
304
+
305
+ signal.signal(signal.SIGTERM, shutdown_handler)
306
+ signal.signal(signal.SIGINT, shutdown_handler)
307
+
308
+ host = os.getenv("HOST", "0.0.0.0")
309
+ port = int(os.getenv("PORT", "8000"))
310
+ reload = os.getenv("RELOAD", "true").lower() == "true"
311
+
312
+ print(f"\n🔧 Starting server on http://{host}:{port}")
313
+ uvicorn.run(
314
+ "main:app",
315
+ host=host,
316
+ port=port,
317
+ reload=reload,
318
+ )
pixi.lock ADDED
Binary file (219 kB). View file
 
pixi.toml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [workspace]
2
+ authors = ["Victor-Iroko <irokovictor7@gmail.com>"]
3
+ channels = ["conda-forge"]
4
+ name = "fastapi-backend"
5
+ platforms = ["win-64", "linux-64"]
6
+ version = "0.1.0"
7
+
8
+ [tasks]
9
+ start = "uvicorn main:app --host 0.0.0.0 --port 8000"
10
+ dev = "uvicorn main:app --reload"
11
+ lint = "ruff check ."
12
+ format = "ruff format ."
13
+ fix = "ruff check . --fix"
14
+ typecheck = "pyright ."
15
+ check = "ruff format . --check && ruff check . && pyright ."
16
+
17
+ [dependencies]
18
+ numpy = ">=1.26.4,<2"
19
+ pandas = ">=2.3.3,<3"
20
+ fastapi = ">=0.128.0,<0.129"
21
+ lightfm = ">=1.17,<2"
22
+ uvicorn = ">=0.40.0,<0.41"
23
+ scikit-learn = ">=1.8.0,<2"
24
+ transformers = ">=4.57.3,<5"
25
+ pytorch = ">=2.9.1,<3"
26
+ psycopg2 = ">=2.9.11,<3"
27
+ huggingface_hub = ">=0.26.0"
28
+
29
+ [pypi-dependencies]
30
+ mab2rec = ">=1.3.1, <2"
31
+ ruff = ">=0.8.0,<1"
32
+ pyright = ">=1.1.0,<2"
33
+ pandas-stubs = ">=2.3.0,<3"
pyrightconfig.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "typeCheckingMode": "basic",
3
+ "exclude": [
4
+ "tests/**",
5
+ "**/__pycache__/**",
6
+ "**/.pixi/**",
7
+ "**/*.pyc",
8
+ "**/.git/**",
9
+ "**/pixi.lock"
10
+ ]
11
+ }
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.26.4,<2
2
+ pandas>=2.3.3,<3
3
+ fastapi>=0.128.0,<0.129
4
+ lightfm>=1.17,<2
5
+ uvicorn>=0.40.0,<0.41
6
+ scikit-learn>=1.8.0,<2
7
+ transformers>=4.57.3,<5
8
+ torch>=2.0.0,<3
9
+ psycopg2-binary>=2.9.11,<3
10
+ huggingface_hub>=0.26.0
11
+ mab2rec>=1.3.1,<2
12
+ python-dotenv>=1.0.0
routes/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Routes package - API endpoint modules."""
2
+
3
+ from routes.movies import router as movies_router
4
+ from routes.songs import router as songs_router
5
+ from routes.recommend import router as recommend_router
6
+ from routes.stress import router as stress_router
7
+
8
+ __all__ = ["movies_router", "songs_router", "recommend_router", "stress_router"]
routes/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (567 Bytes). View file
 
routes/__pycache__/movies.cpython-311.pyc ADDED
Binary file (6.89 kB). View file
 
routes/__pycache__/recommend.cpython-311.pyc ADDED
Binary file (28.8 kB). View file
 
routes/__pycache__/recommend.cpython-314.pyc ADDED
Binary file (26.9 kB). View file
 
routes/__pycache__/songs.cpython-311.pyc ADDED
Binary file (12.2 kB). View file
 
routes/__pycache__/stress.cpython-311.pyc ADDED
Binary file (4.15 kB). View file