File size: 3,215 Bytes
6cda091
e317d56
6cda091
e317d56
 
6cda091
 
e317d56
 
6cda091
 
 
e317d56
6cda091
e317d56
 
6cda091
e317d56
 
6cda091
e317d56
6cda091
e317d56
 
 
6cda091
 
 
 
e317d56
6cda091
 
 
 
e317d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6cda091
 
e317d56
 
 
 
6cda091
e317d56
6cda091
e317d56
 
6cda091
 
 
e317d56
 
 
 
6cda091
 
 
 
e317d56
 
 
 
 
 
 
 
6cda091
 
e317d56
 
 
 
6cda091
 
e317d56
6cda091
 
e317d56
 
 
 
 
 
6cda091
e317d56
 
 
 
6cda091
e317d56
 
 
 
 
6cda091
e317d56
6cda091
 
e317d56
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
112
113
114
115
"""
youtube_hate_detector API

Run: uv run uvicorn src.api.main:app --reload --port 8000
Docs: http://localhost:8000/docs
"""

from __future__ import annotations

import os
import time
from contextlib import asynccontextmanager
from pathlib import Path

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

load_dotenv()

from src.api.routes import health, models, predict, videos
from src.api.state import PROJECT_ROOT, get_state
from src.service.model_service import AVAILABLE_MODELS, ModelService, check_model_availability
from src.utils.logger import get_logger

logger = get_logger(__name__)

FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"


@asynccontextmanager
async def lifespan(app: FastAPI):
    state = get_state()
    model_name = os.getenv("MODEL_NAME", next(iter(AVAILABLE_MODELS.keys())))
    available, reason = check_model_availability(model_name, PROJECT_ROOT)
    if not available:
        fallback = next(iter(AVAILABLE_MODELS.keys()))
        logger.warning(
            "MODEL_NAME '%s' unavailable (%s) — using '%s'",
            model_name,
            reason,
            fallback,
        )
        model_name = fallback
    logger.info("Starting youtube_hate_detector API — model: %s", model_name)
    state["service"] = ModelService(model_name, PROJECT_ROOT)
    state["model_name"] = model_name
    state["startup_time"] = time.time()
    state["predictions_served"] = 0

    try:
        state["service"].predict("warmup")
        logger.info("Model warm-up complete")
    except Exception as exc:
        logger.warning("Warm-up failed (non-critical): %s", exc)

    yield

    state["service"] = None
    logger.info("API shutdown")


app = FastAPI(
    title="youtube_hate_detector API",
    description="Toxic comment detection for YouTube-style moderation demos",
    version="1.0.0",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
        "http://127.0.0.1:5173",
        "http://localhost:8000",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(health.router)
app.include_router(models.router)
app.include_router(predict.router)
app.include_router(videos.router)


_API_GET_PREFIXES = ("models", "model", "videos", "predict", "health", "docs", "redoc", "openapi")


def _mount_frontend() -> None:
    if not FRONTEND_DIST.is_dir():
        return
    assets = FRONTEND_DIST / "assets"
    if assets.is_dir():
        app.mount("/assets", StaticFiles(directory=assets), name="assets")

    @app.get("/{full_path:path}", include_in_schema=False)
    async def spa_fallback(full_path: str):
        if full_path.startswith(_API_GET_PREFIXES):
            from fastapi import HTTPException

            raise HTTPException(status_code=404, detail="Not found")
        index = FRONTEND_DIST / "index.html"
        if index.exists():
            return FileResponse(index)
        from fastapi import HTTPException

        raise HTTPException(status_code=404, detail="Not found")


_mount_frontend()