| from fastapi import FastAPI, HTTPException, BackgroundTasks |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel, Field |
| from typing import List, Optional |
| import os |
| import uvicorn |
|
|
| |
| from src.recommender import MovieRecommender |
| from src.ingest import ingest_high_quality_movies |
|
|
| |
| app = FastAPI( |
| title="CineMatch API", |
| description="A content-based movie recommender using FAISS & Transformers.", |
| version="2.0.0" |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["https://aether-match.vercel.app"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| rec_engine: Optional[MovieRecommender] = None |
|
|
| |
| @app.on_event("startup") |
| async def startup_event(): |
| """ |
| Load the heavy AI model once when the server starts. |
| """ |
| global rec_engine |
| rec_engine = MovieRecommender() |
| |
| if os.path.exists('models/movie_index.faiss'): |
| print(" [INFO] Loading AI Model from disk...") |
| rec_engine.load('models/') |
| |
| count = rec_engine.index.ntotal if rec_engine.index else 0 |
| print(f" [INFO] Model loaded. Index contains {count} movies.") |
| else: |
| print(" [WARNING] No model found at 'models/'. API will return errors until ingestion is run.") |
|
|
| |
| class SearchRequest(BaseModel): |
| query: str |
| k: int = 10 |
|
|
| class VibeRequest(BaseModel): |
| tags: List[str] = [] |
| description: str = "" |
| k: int = 10 |
|
|
| class UserHistoryRequest(BaseModel): |
| liked_movies: List[str] |
| k: int = 10 |
|
|
| class MovieResponse(BaseModel): |
| |
| id: int |
| title: str |
| score: float |
|
|
| |
| def check_model(): |
| if not rec_engine or not rec_engine.index: |
| raise HTTPException(status_code=503, detail="AI Model is not loaded. Run ingestion first.") |
|
|
| |
|
|
| @app.get("/") |
| def health_check(): |
| """Simple check to see if server is running.""" |
| loaded = rec_engine is not None and rec_engine.index is not None |
| return {"status": "online", "model_loaded": loaded} |
|
|
| @app.post("/search", response_model=List[MovieResponse]) |
| def search_movies(request: SearchRequest): |
| """ |
| Semantic Search: Convert query to vector -> Find nearest movies. |
| Now includes Guardrails automatically via the Recommender class. |
| """ |
| check_model() |
| results = rec_engine.recommend(request.query, k=request.k) |
| return results |
|
|
| @app.post("/recommend/vibe", response_model=dict) |
| def vibe_check(request: VibeRequest): |
| """ |
| Recommends based on a mix of Tags and Description. |
| """ |
| check_model() |
| |
| |
| |
| tag_str = " ".join(request.tags) * 2 |
| query_soup = f"{tag_str} {request.description}".strip() |
| |
| if not query_soup: |
| raise HTTPException(status_code=400, detail="Please provide at least one tag or description.") |
|
|
| |
| results = rec_engine.recommend(query_soup, k=request.k) |
| |
| return { |
| "interpreted_query": query_soup, |
| "results": results |
| } |
|
|
| @app.post("/recommend/user", response_model=List[MovieResponse]) |
| def recommend_for_user(request: UserHistoryRequest): |
| """ |
| Takes a list of movie titles the user likes, averages their vectors, |
| and finds similar movies. |
| """ |
| check_model() |
| results = rec_engine.recommend_for_user(request.liked_movies, k=request.k) |
| |
| |
| if not results: |
| return [] |
| |
| return results |
|
|
| @app.get("/recommend/movie/{title}", response_model=List[MovieResponse]) |
| def recommend_similar_movie(title: str): |
| """ |
| Finds movies similar to a specific title. |
| We reuse 'recommend_for_user' logic passing a single movie. |
| """ |
| check_model() |
| |
| results = rec_engine.recommend_for_user([title], k=10) |
| |
| if not results: |
| raise HTTPException(status_code=404, detail=f"Movie '{title}' not found in database.") |
| |
| return results |
|
|
| |
|
|
| def background_update_task(): |
| """ |
| Runs the ingestion script and reloads the model in memory. |
| """ |
| print(" [BACKGROUND] Starting update process...") |
| |
| |
| try: |
| ingest_high_quality_movies(target_count=50, reset=False) |
| print(" [BACKGROUND] Ingestion complete.") |
| except Exception as e: |
| print(f" [ERROR] Ingestion failed: {e}") |
| return |
|
|
| |
| print(" [BACKGROUND] Reloading model into RAM...") |
| rec_engine.load('models/') |
| print(" [BACKGROUND] Update complete. Model reloaded.") |
|
|
| @app.post("/update") |
| def trigger_update(background_tasks: BackgroundTasks): |
| """ |
| Manually triggers the 'Weekly Update' logic. |
| Runs in the background so the API doesn't freeze. |
| """ |
| background_tasks.add_task(background_update_task) |
| return {"message": "Update process started in background (Append Mode)."} |
|
|
| |
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=8000) |
|
|