File size: 3,303 Bytes
22b7d63 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | """
main.py — the CineMatch FastAPI service.
Routes (see app/schemas.py for the full contract):
GET /api/health engine + model status
GET /api/stats catalog facets for the empty state
GET /api/suggest?q=&limit= title autocomplete
POST /api/search hybrid search (SearchRequest -> SearchResponse)
GET /api/movie/{id} single movie
GET /api/movie/{id}/similar?limit= "More like this"
"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.engine.search import SearchEngine
from app.schemas import (
HealthResponse,
Movie,
SearchRequest,
SearchResponse,
StatsResponse,
Suggestion,
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("cinematch")
state: dict[str, SearchEngine] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("loading engine ...")
engine = SearchEngine(settings)
state["engine"] = engine
log.info("corpus=%d slm=%s loading models (first run downloads them) ...",
engine.corpus.size, engine.slm.label)
try:
engine.warmup()
log.info("ready. reranker=%s", bool(engine.reranker and engine.reranker.available))
except Exception as e: # pragma: no cover - model download failure shouldn't kill server
log.warning("warmup issue (engine still serves): %s", e)
yield
state.clear()
app = FastAPI(title="CineMatch API", version="2.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
# Public, read-only, no-credentials API — allow any origin so the deployed
# frontend (Vercel, HF Space, localhost, …) can call it from the browser.
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
def engine() -> SearchEngine:
eng = state.get("engine")
if eng is None:
raise HTTPException(503, "engine not ready")
return eng
@app.get("/")
def root():
return {"service": "CineMatch API", "docs": "/docs", "health": "/api/health"}
@app.get("/api/health", response_model=HealthResponse)
def health():
return engine().health()
@app.get("/api/stats", response_model=StatsResponse)
def stats():
return engine().stats()
@app.get("/api/suggest", response_model=list[Suggestion])
def suggest(q: str = Query(..., min_length=1), limit: int = Query(8, ge=1, le=20)):
return engine().suggest(q, limit)
@app.post("/api/search", response_model=SearchResponse)
def search(req: SearchRequest):
return engine().search(req)
@app.get("/api/movie/{movie_id}", response_model=Movie)
def get_movie(movie_id: str):
try:
return engine().get_movie(movie_id)
except KeyError:
raise HTTPException(404, f"movie '{movie_id}' not found")
@app.get("/api/movie/{movie_id}/similar", response_model=SearchResponse)
def similar(movie_id: str, limit: int = Query(12, ge=1, le=50)):
try:
return engine().similar(movie_id, limit)
except KeyError:
raise HTTPException(404, f"movie '{movie_id}' not found")
|