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()
|