| """ |
| schemas.py — The CineMatch API contract (single source of truth). |
| |
| Every request/response body in the FastAPI layer is one of these pydantic |
| models. The frontend's TypeScript types in `frontend/lib/types.ts` mirror |
| these one-to-one. Change them here first, then propagate. |
| """ |
| from __future__ import annotations |
|
|
| from typing import Literal, Optional |
|
|
| from pydantic import BaseModel, Field |
|
|
|
|
| |
| |
| |
| class Movie(BaseModel): |
| id: str |
| tmdb_id: Optional[int] = None |
| title: str |
| year: Optional[int] = None |
| genres: list[str] = [] |
| overview: str = "" |
| tagline: Optional[str] = None |
| poster_url: Optional[str] = None |
| vote_average: Optional[float] = None |
| vote_count: int = 0 |
| popularity: float = 0.0 |
| language: Optional[str] = None |
| cast: list[str] = [] |
| director: Optional[str] = None |
| keywords: list[str] = [] |
| runtime: Optional[int] = None |
|
|
|
|
| |
| |
| |
| class ScoreBreakdown(BaseModel): |
| """All sub-scores are min-max normalized to 0..1 within the candidate set |
| so the UI can render comparable bars.""" |
| lexical: float = 0.0 |
| semantic: float = 0.0 |
| fusion: float = 0.0 |
| rerank: Optional[float] = None |
| lexical_rank: Optional[int] = None |
| semantic_rank: Optional[int] = None |
|
|
|
|
| class SearchResult(Movie): |
| score: float = 0.0 |
| scores: ScoreBreakdown = ScoreBreakdown() |
| match_terms: list[str] = [] |
| reason: Optional[str] = None |
|
|
|
|
| |
| |
| |
| class ParsedQuery(BaseModel): |
| intent: Literal["recommend", "find_title", "similar"] = "recommend" |
| clean_query: str = "" |
| genres: list[str] = [] |
| moods: list[str] = [] |
| era_from: Optional[int] = None |
| era_to: Optional[int] = None |
| similar_to: Optional[str] = None |
| keywords: list[str] = [] |
| negations: list[str] = [] |
| min_rating: Optional[float] = None |
| source: str = "rules" |
|
|
|
|
| class Timing(BaseModel): |
| parse: float = 0.0 |
| lexical: float = 0.0 |
| semantic: float = 0.0 |
| fusion: float = 0.0 |
| rerank: float = 0.0 |
| reason: float = 0.0 |
| total: float = 0.0 |
|
|
|
|
| class EngineInfo(BaseModel): |
| fusion: str |
| rerank: bool |
| embedding_model: str |
| reranker_model: Optional[str] = None |
| slm: str |
| corpus_size: int |
|
|
|
|
| |
| |
| |
| class SearchRequest(BaseModel): |
| query: str = Field(..., min_length=1, max_length=400) |
| limit: int = Field(12, ge=1, le=50) |
| fusion: Literal["rrf", "weighted"] = "rrf" |
| lexical_weight: float = Field(0.5, ge=0.0, le=1.0) |
| semantic_weight: float = Field(0.5, ge=0.0, le=1.0) |
| rerank: bool = True |
| explain: bool = True |
|
|
|
|
| class SearchResponse(BaseModel): |
| query: str |
| parsed: ParsedQuery |
| engine: EngineInfo |
| timing_ms: Timing |
| count: int |
| results: list[SearchResult] |
|
|
|
|
| class Suggestion(BaseModel): |
| id: str |
| title: str |
| year: Optional[int] = None |
| poster_url: Optional[str] = None |
|
|
|
|
| class GenreCount(BaseModel): |
| name: str |
| count: int |
|
|
|
|
| class StatsResponse(BaseModel): |
| corpus_size: int |
| genres: list[GenreCount] |
| decades: list[GenreCount] |
| sample_queries: list[str] |
| top_rated: list[Suggestion] |
|
|
|
|
| class HealthResponse(BaseModel): |
| status: str |
| ready: bool |
| corpus_size: int |
| embedding_model: str |
| reranker: bool |
| reranker_model: Optional[str] = None |
| slm: str |
| faiss: bool |
|
|