| """FastAPI Recommendation Server |
| |
| This server provides endpoints for movie and song recommendations. |
| Models are loaded on startup using FastAPI's lifespan events. |
| |
| Run with: uvicorn main:app --reload |
| """ |
|
|
| import os |
| import traceback |
| from contextlib import asynccontextmanager |
| from pathlib import Path |
| from typing import AsyncGenerator |
|
|
| from dotenv import load_dotenv |
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
|
|
| ENV_FILE = Path(__file__).parent.parent / ".env" |
| load_dotenv(ENV_FILE) |
|
|
| from routes.movies import router as movies_router |
| from routes.songs import router as songs_router |
| from routes.stress import router as analyze_router |
| from routes.recommend import router as recommend_router |
| from core.schemas import HealthCheckResponse |
| from services.movie_recommender import MovieRecommender |
| from services.song_recommender import SongRecommender |
| from services.stress_detector import StressDetector |
| from services.emotion_detector import EmotionDetector |
| from services.contextual_bandit import HierarchicalBandit |
|
|
|
|
| |
| |
| |
|
|
| API_VERSION = "1.0.0" |
| API_TITLE = "Nostalgic Recommendation API" |
| API_DESCRIPTION = """ |
| A recommendation API for movies and songs using machine learning models. |
| |
| ## Features |
| |
| - π¬ **Movie Recommendations**: Using LightFM collaborative filtering with cold-start support |
| - π΅ **Song Recommendations**: Using content-based filtering with pgvector similarity search |
| - π **Search**: Search movies by title, songs by name or artist |
| - β¨ **Validation**: Full request/response validation with Pydantic |
| |
| ## Models |
| |
| - **Movie Recommender**: LightFM model trained on MovieLens 32M dataset |
| - **Song Recommender**: Content-based filtering using audio features, stored in pgvector |
| """ |
|
|
| |
| CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",") |
|
|
|
|
| |
| |
| |
|
|
|
|
| class AppState: |
| """Application state container for recommenders.""" |
|
|
| movie_recommender: MovieRecommender | None = None |
| song_recommender: SongRecommender | None = None |
| movie_model_loaded: bool = False |
| song_model_loaded: bool = False |
|
|
|
|
| |
| |
| |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: |
| """ |
| Manage application lifespan. |
| |
| On startup: Load movie and song recommendation models. |
| On shutdown: Clean up resources. |
| """ |
| print("=" * 60) |
| print(f"π Starting {API_TITLE} v{API_VERSION}") |
| print("=" * 60) |
|
|
| |
| app.state.recommenders = { |
| "movie": None, |
| "song": None, |
| "stress": None, |
| "emotion": None, |
| "bandit": None, |
| } |
| app.state.model_status = { |
| "movie_loaded": False, |
| "song_loaded": False, |
| "stress_loaded": False, |
| "emotion_loaded": False, |
| "bandit_loaded": False, |
| } |
|
|
| |
| print("\nπ½οΈ Loading Movie Recommender...") |
| try: |
| movie_recommender = MovieRecommender() |
| app.state.recommenders["movie"] = movie_recommender |
| app.state.model_status["movie_loaded"] = True |
| print("β
Movie Recommender loaded successfully!") |
| except FileNotFoundError as e: |
| print(f"β οΈ Movie model files not found: {e}") |
| print(" Movie recommendations will be unavailable.") |
| except Exception as e: |
| print(f"β Error loading Movie Recommender: {e}") |
| traceback.print_exc() |
|
|
| |
| print("\nπ΅ Loading Song Recommender...") |
| try: |
| song_recommender = SongRecommender() |
| app.state.recommenders["song"] = song_recommender |
| app.state.model_status["song_loaded"] = True |
| print("β
Song Recommender loaded successfully!") |
| except FileNotFoundError as e: |
| print(f"β οΈ Song model files not found: {e}") |
| print(" Song recommendations will be unavailable.") |
| except Exception as e: |
| print(f"β Error loading Song Recommender: {e}") |
| print(" This may be due to database connection issues.") |
| traceback.print_exc() |
|
|
| |
| print("\nπ§ Loading Stress Detector...") |
| try: |
| stress_detector = StressDetector() |
| app.state.recommenders["stress"] = stress_detector |
| app.state.model_status["stress_loaded"] = True |
| print("β
Stress Detector loaded successfully!") |
| except FileNotFoundError as e: |
| print(f"β οΈ Stress model files not found: {e}") |
| print(" Stress detection will be unavailable.") |
| except Exception as e: |
| print(f"β Error loading Stress Detector: {e}") |
| traceback.print_exc() |
|
|
| |
| print("\nπ Loading Emotion Detector...") |
| try: |
| emotion_detector = EmotionDetector(use_mock=False) |
| app.state.recommenders["emotion"] = emotion_detector |
| app.state.model_status["emotion_loaded"] = True |
| print("β
Emotion Detector loaded successfully!") |
| except Exception as e: |
| print(f"β οΈ Error loading real Emotion Detector: {e}") |
| print(" Falling back to MOCK Emotion Detector.") |
| try: |
| emotion_detector = EmotionDetector(use_mock=True) |
| app.state.recommenders["emotion"] = emotion_detector |
| app.state.model_status["emotion_loaded"] = True |
| print("β
Mock Emotion Detector loaded.") |
| except Exception as e2: |
| print(f"β Error loading Mock Emotion Detector: {e2}") |
| traceback.print_exc() |
|
|
| |
| print("\nπ° Loading Contextual Bandit...") |
| try: |
| bandit = HierarchicalBandit() |
| app.state.recommenders["bandit"] = bandit |
| app.state.model_status["bandit_loaded"] = True |
| print("β
Contextual Bandit loaded successfully!") |
| except Exception as e: |
| print(f"β Error loading Contextual Bandit: {e}") |
| traceback.print_exc() |
|
|
| print("\n" + "=" * 60) |
| print("π Server is ready to accept requests") |
| print("=" * 60 + "\n") |
|
|
| |
| yield |
|
|
| |
| print("\nπ Shutting down server...") |
|
|
| |
| if app.state.recommenders["movie"]: |
| app.state.recommenders["movie"].close() |
| print(" Closed movie recommender database connection.") |
|
|
| if app.state.recommenders["song"]: |
| app.state.recommenders["song"].close() |
| print(" Closed song recommender database connection.") |
|
|
| if app.state.recommenders["stress"]: |
| app.state.recommenders["stress"].close() |
| print(" Closed stress detector.") |
|
|
| if app.state.recommenders["emotion"]: |
| app.state.recommenders["emotion"].close() |
| print(" Closed emotion detector.") |
|
|
| if app.state.recommenders["bandit"]: |
| app.state.recommenders["bandit"].close() |
| print(" Closed contextual bandit.") |
|
|
| |
| app.state.recommenders["movie"] = None |
| app.state.recommenders["song"] = None |
| app.state.recommenders["stress"] = None |
| app.state.recommenders["emotion"] = None |
| app.state.recommenders["bandit"] = None |
|
|
| print("π Server shutdown complete.\n") |
|
|
|
|
| |
| |
| |
|
|
| app = FastAPI( |
| title=API_TITLE, |
| description=API_DESCRIPTION, |
| version=API_VERSION, |
| lifespan=lifespan, |
| docs_url="/docs", |
| redoc_url="/redoc", |
| openapi_url="/openapi.json", |
| ) |
|
|
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=CORS_ORIGINS, |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @app.get( |
| "/health", |
| response_model=HealthCheckResponse, |
| tags=["Health"], |
| summary="Health check", |
| description="Check the API status and model availability.", |
| ) |
| async def health_check() -> HealthCheckResponse: |
| """Return the API health status and model loading status.""" |
| return HealthCheckResponse( |
| status="healthy", |
| movie_model_loaded=app.state.model_status.get("movie_loaded", False), |
| song_model_loaded=app.state.model_status.get("song_loaded", False), |
| stress_model_loaded=app.state.model_status.get("stress_loaded", False), |
| emotion_model_loaded=app.state.model_status.get("emotion_loaded", False), |
| bandit_loaded=app.state.model_status.get("bandit_loaded", False), |
| version=API_VERSION, |
| ) |
|
|
|
|
| @app.get("/", tags=["Root"]) |
| async def root() -> dict[str, str]: |
| """Root endpoint with welcome message.""" |
| return { |
| "message": f"Welcome to {API_TITLE}", |
| "version": API_VERSION, |
| "docs": "/docs", |
| "health": "/health", |
| } |
|
|
|
|
| |
| |
| |
|
|
| app.include_router(movies_router) |
| app.include_router(songs_router) |
| app.include_router(analyze_router) |
| app.include_router(recommend_router) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import signal |
| import sys |
| import uvicorn |
|
|
| def shutdown_handler(signum, frame): |
| print(f"\nπ Received signal {signum}, initiating graceful shutdown...") |
| sys.exit(0) |
|
|
| signal.signal(signal.SIGTERM, shutdown_handler) |
| signal.signal(signal.SIGINT, shutdown_handler) |
|
|
| host = os.getenv("HOST", "0.0.0.0") |
| port = int(os.getenv("PORT", "8000")) |
| reload = os.getenv("RELOAD", "true").lower() == "true" |
|
|
| print(f"\nπ§ Starting server on http://{host}:{port}") |
| uvicorn.run( |
| "main:app", |
| host=host, |
| port=port, |
| reload=reload, |
| ) |
|
|