Update backend/app.py
Browse files- backend/app.py +33 -20
backend/app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.staticfiles import StaticFiles
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -8,16 +9,26 @@ import numpy as np
|
|
| 8 |
import os
|
| 9 |
from huggingface_hub import hf_hub_download
|
| 10 |
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
model_assets = {}
|
| 13 |
|
| 14 |
@asynccontextmanager
|
| 15 |
async def lifespan(app: FastAPI):
|
| 16 |
-
|
| 17 |
try:
|
| 18 |
REPO_ID = "SateeshAmbesange/PragyanAI-Super30_ReactJS_RecommendationSystem"
|
| 19 |
use_auth_token = os.environ.get("HUGGING_FACE_HUB_TOKEN") is not None
|
| 20 |
-
|
| 21 |
|
| 22 |
movies_file = hf_hub_download(repo_id=REPO_ID, filename="movies_df.joblib", use_auth_token=use_auth_token)
|
| 23 |
cosine_file = hf_hub_download(repo_id=REPO_ID, filename="cosine_sim_matrix.joblib", use_auth_token=use_auth_token)
|
|
@@ -26,22 +37,24 @@ async def lifespan(app: FastAPI):
|
|
| 26 |
model_assets['cosine_sim'] = joblib.load(cosine_file)
|
| 27 |
model_assets['indices'] = pd.Series(model_assets['movies_df'].index, index=model_assets['movies_df']['title'])
|
| 28 |
|
| 29 |
-
|
| 30 |
except Exception as e:
|
| 31 |
-
|
| 32 |
model_assets['movies_df'] = None
|
| 33 |
model_assets['cosine_sim'] = None
|
| 34 |
model_assets['indices'] = None
|
| 35 |
-
raise e
|
|
|
|
| 36 |
yield
|
| 37 |
-
|
|
|
|
| 38 |
model_assets.clear()
|
| 39 |
|
| 40 |
app = FastAPI(lifespan=lifespan)
|
| 41 |
|
| 42 |
app.add_middleware(
|
| 43 |
CORSMiddleware,
|
| 44 |
-
allow_origins=["*"],
|
| 45 |
allow_credentials=True,
|
| 46 |
allow_methods=["*"],
|
| 47 |
allow_headers=["*"],
|
|
@@ -49,28 +62,29 @@ app.add_middleware(
|
|
| 49 |
|
| 50 |
@app.get("/api/movies")
|
| 51 |
async def get_movie_list():
|
| 52 |
-
|
| 53 |
movies_df = model_assets.get('movies_df')
|
| 54 |
if movies_df is None:
|
| 55 |
-
|
| 56 |
raise HTTPException(status_code=503, detail="Model assets not available.")
|
|
|
|
| 57 |
titles = movies_df['title'].sort_values().unique().tolist()
|
| 58 |
-
|
| 59 |
return {"movies": titles}
|
| 60 |
|
| 61 |
@app.get("/api/recommend")
|
| 62 |
async def get_recommendations_api(title: str):
|
| 63 |
-
|
| 64 |
movies_df = model_assets.get('movies_df')
|
| 65 |
cosine_sim = model_assets.get('cosine_sim')
|
| 66 |
indices = model_assets.get('indices')
|
| 67 |
|
| 68 |
if not movies_df or not cosine_sim or not indices:
|
| 69 |
-
|
| 70 |
-
raise HTTPException(status_code=503, detail="Model assets not available.")
|
| 71 |
|
| 72 |
if title not in indices:
|
| 73 |
-
|
| 74 |
raise HTTPException(status_code=404, detail="Movie not found")
|
| 75 |
|
| 76 |
try:
|
|
@@ -82,17 +96,16 @@ async def get_recommendations_api(title: str):
|
|
| 82 |
return score if isinstance(score, (int, float, np.number)) and np.isfinite(score) else -1
|
| 83 |
|
| 84 |
sim_scores = sorted(sim_scores, key=sort_key, reverse=True)
|
| 85 |
-
sim_scores = sim_scores[1:11] #
|
| 86 |
|
| 87 |
movie_indices = [i[0] for i in sim_scores]
|
| 88 |
recommendations = movies_df.iloc[movie_indices][['title', 'id']].to_dict(orient='records')
|
| 89 |
|
| 90 |
-
|
| 91 |
return {"recommendations": recommendations}
|
| 92 |
except Exception as e:
|
| 93 |
-
|
| 94 |
raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
|
| 95 |
|
| 96 |
-
# Serve React static files from 'static'
|
| 97 |
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
| 98 |
-
|
|
|
|
| 1 |
+
import logging
|
| 2 |
from fastapi import FastAPI, HTTPException
|
| 3 |
from fastapi.staticfiles import StaticFiles
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 9 |
import os
|
| 10 |
from huggingface_hub import hf_hub_download
|
| 11 |
|
| 12 |
+
# Configure logging to file and stdout
|
| 13 |
+
logging.basicConfig(
|
| 14 |
+
level=logging.DEBUG,
|
| 15 |
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
| 16 |
+
handlers=[
|
| 17 |
+
logging.FileHandler("space_runtime.log"),
|
| 18 |
+
logging.StreamHandler()
|
| 19 |
+
]
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Global variable to hold model assets
|
| 23 |
model_assets = {}
|
| 24 |
|
| 25 |
@asynccontextmanager
|
| 26 |
async def lifespan(app: FastAPI):
|
| 27 |
+
logging.info("STARTUP: Loading model assets...")
|
| 28 |
try:
|
| 29 |
REPO_ID = "SateeshAmbesange/PragyanAI-Super30_ReactJS_RecommendationSystem"
|
| 30 |
use_auth_token = os.environ.get("HUGGING_FACE_HUB_TOKEN") is not None
|
| 31 |
+
logging.info(f"Downloading model files from {REPO_ID} with auth token: {use_auth_token}")
|
| 32 |
|
| 33 |
movies_file = hf_hub_download(repo_id=REPO_ID, filename="movies_df.joblib", use_auth_token=use_auth_token)
|
| 34 |
cosine_file = hf_hub_download(repo_id=REPO_ID, filename="cosine_sim_matrix.joblib", use_auth_token=use_auth_token)
|
|
|
|
| 37 |
model_assets['cosine_sim'] = joblib.load(cosine_file)
|
| 38 |
model_assets['indices'] = pd.Series(model_assets['movies_df'].index, index=model_assets['movies_df']['title'])
|
| 39 |
|
| 40 |
+
logging.info(f"Model assets loaded successfully: {len(model_assets['movies_df'])} movies")
|
| 41 |
except Exception as e:
|
| 42 |
+
logging.error(f"FATAL ERROR: Could not load model assets: {e}", exc_info=True)
|
| 43 |
model_assets['movies_df'] = None
|
| 44 |
model_assets['cosine_sim'] = None
|
| 45 |
model_assets['indices'] = None
|
| 46 |
+
raise e
|
| 47 |
+
|
| 48 |
yield
|
| 49 |
+
|
| 50 |
+
logging.info("SHUTDOWN: Clearing model assets.")
|
| 51 |
model_assets.clear()
|
| 52 |
|
| 53 |
app = FastAPI(lifespan=lifespan)
|
| 54 |
|
| 55 |
app.add_middleware(
|
| 56 |
CORSMiddleware,
|
| 57 |
+
allow_origins=["*"], # For production, restrict this to your frontend domain
|
| 58 |
allow_credentials=True,
|
| 59 |
allow_methods=["*"],
|
| 60 |
allow_headers=["*"],
|
|
|
|
| 62 |
|
| 63 |
@app.get("/api/movies")
|
| 64 |
async def get_movie_list():
|
| 65 |
+
logging.debug("API CALL: /api/movies")
|
| 66 |
movies_df = model_assets.get('movies_df')
|
| 67 |
if movies_df is None:
|
| 68 |
+
logging.error("Model assets unavailable in /api/movies")
|
| 69 |
raise HTTPException(status_code=503, detail="Model assets not available.")
|
| 70 |
+
|
| 71 |
titles = movies_df['title'].sort_values().unique().tolist()
|
| 72 |
+
logging.debug(f"API RESPONSE: Serving {len(titles)} movie titles")
|
| 73 |
return {"movies": titles}
|
| 74 |
|
| 75 |
@app.get("/api/recommend")
|
| 76 |
async def get_recommendations_api(title: str):
|
| 77 |
+
logging.debug(f"API CALL: /api/recommend for title '{title}'")
|
| 78 |
movies_df = model_assets.get('movies_df')
|
| 79 |
cosine_sim = model_assets.get('cosine_sim')
|
| 80 |
indices = model_assets.get('indices')
|
| 81 |
|
| 82 |
if not movies_df or not cosine_sim or not indices:
|
| 83 |
+
logging.error("Model assets missing in /api/recommend")
|
| 84 |
+
raise HTTPException(status_code=503, detail="Model assets are not available.")
|
| 85 |
|
| 86 |
if title not in indices:
|
| 87 |
+
logging.error(f"Movie title '{title}' not found in indices")
|
| 88 |
raise HTTPException(status_code=404, detail="Movie not found")
|
| 89 |
|
| 90 |
try:
|
|
|
|
| 96 |
return score if isinstance(score, (int, float, np.number)) and np.isfinite(score) else -1
|
| 97 |
|
| 98 |
sim_scores = sorted(sim_scores, key=sort_key, reverse=True)
|
| 99 |
+
sim_scores = sim_scores[1:11] # Exclude the queried movie itself
|
| 100 |
|
| 101 |
movie_indices = [i[0] for i in sim_scores]
|
| 102 |
recommendations = movies_df.iloc[movie_indices][['title', 'id']].to_dict(orient='records')
|
| 103 |
|
| 104 |
+
logging.debug(f"API RESPONSE: Returning {len(recommendations)} recommendations for '{title}'")
|
| 105 |
return {"recommendations": recommendations}
|
| 106 |
except Exception as e:
|
| 107 |
+
logging.error(f"Exception in /api/recommend: {e}", exc_info=True)
|
| 108 |
raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
|
| 109 |
|
| 110 |
+
# Serve React frontend static files from 'static' folder
|
| 111 |
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
|
|