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