File size: 3,494 Bytes
6cda091
e317d56
6cda091
e317d56
 
6cda091
 
e317d56
 
6cda091
 
 
e317d56
6cda091
e317d56
 
6cda091
e317d56
 
6cda091
e317d56
6cda091
e317d56
 
46cc63a
 
 
 
 
 
6cda091
 
 
 
e317d56
6cda091
 
 
 
e317d56
46cc63a
e317d56
 
46cc63a
 
 
e317d56
 
 
 
 
 
 
 
 
 
 
 
6cda091
 
e317d56
 
 
 
6cda091
e317d56
6cda091
e317d56
 
6cda091
 
 
e317d56
 
 
 
6cda091
 
 
 
e317d56
 
 
 
 
 
 
 
6cda091
 
e317d56
 
 
 
6cda091
 
0f0ce9b
 
 
 
 
 
 
 
6cda091
 
e317d56
 
 
 
 
 
6cda091
e317d56
 
0f0ce9b
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
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,
    _DEFAULT_MODEL_NAME,
    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", _DEFAULT_MODEL_NAME)
    available, reason = check_model_availability(model_name, PROJECT_ROOT)
    if not available:
        fallback = _DEFAULT_MODEL_NAME
        if not check_model_availability(fallback, PROJECT_ROOT)[0]:
            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_PATH_ROOTS = frozenset(
    {"models", "model", "videos", "predict", "health", "docs", "redoc", "openapi"}
)


def _is_api_spa_path(full_path: str) -> bool:
    root = full_path.split("/")[0] if full_path else ""
    return root in _API_PATH_ROOTS


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 _is_api_spa_path(full_path):
            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()