File size: 5,412 Bytes
6c9b8f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1c84b5
6c9b8f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954286a
6c9b8f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1c84b5
 
 
6c9b8f1
b1c84b5
 
 
 
 
 
 
6c9b8f1
 
 
 
 
 
 
 
 
 
 
 
 
 
b1c84b5
32ac1d9
6c9b8f1
32ac1d9
b1c84b5
32ac1d9
6c9b8f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
130
131
132
133
134
135
136
137
138
139
140
"""
PhilVerify β€” FastAPI Application Entry Point
Run: uvicorn main:app --reload --port 8000
Docs: http://localhost:8000/docs
"""
import logging
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from config import get_settings
from api.routes.verify import router as verify_router
from api.routes.history import router as history_router
from api.routes.trends import router as trends_router
from api.routes.preview import router as preview_router

# ── Logging ───────────────────────────────────────────────────────────────────
logging.basicConfig(
    level=getattr(logging, get_settings().log_level.upper(), logging.INFO),
    format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
)
logger = logging.getLogger("philverify")


# ── Lifespan (startup / shutdown) ─────────────────────────────────────────────

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Warm up NLP models on startup so first request isn't slow."""
    logger.info("πŸš€ PhilVerify starting up...")
    try:
        # Lazy-import to avoid crashing if heavy deps not yet installed
        from nlp.language_detector import LanguageDetector
        from nlp.preprocessor import TextPreprocessor
        from ml.tfidf_classifier import TFIDFClassifier

        app.state.preprocessor = TextPreprocessor()
        app.state.language_detector = LanguageDetector()
        classifier = TFIDFClassifier()
        classifier.train()          # Trains on seed dataset if model not persisted
        app.state.classifier = classifier

        logger.info("βœ… NLP models ready")
    except ImportError as e:
        logger.warning("⚠️  Some NLP modules not installed yet: %s β€” stubs will be used", e)

    yield  # ── App is running ──

    logger.info("πŸ‘‹ PhilVerify shutting down")


# ── App ───────────────────────────────────────────────────────────────────────

settings = get_settings()

app = FastAPI(
    title="PhilVerify API",
    description=(
        "Multimodal fake news detection for Philippine social media. "
        "Supports text, URL, image (OCR), and video (Whisper ASR) inputs."
    ),
    version="0.1.0",
    docs_url="/docs",
    redoc_url="/redoc",
    lifespan=lifespan,
)


# ── CORS ──────────────────────────────────────────────────────────────────────

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins_list,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ── Global Error Handler ──────────────────────────────────────────────────────

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    logger.exception("Unhandled error on %s %s: %s", request.method, request.url.path, exc)
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={"error": "Internal server error", "detail": str(exc)},
    )


# ── Routers (all under /api so Firebase Hosting rewrite ↔ Cloud Run match) ────
# Frontend calls /api/verify/..., Firebase Hosting forwards full path to Cloud Run.
# In dev, Vite proxy forwards /api/... directly without stripping β€” so same prefix.

from fastapi import APIRouter as _APIRouter
_api = _APIRouter(prefix="/api")
_api.include_router(verify_router)
_api.include_router(history_router)
_api.include_router(trends_router)
_api.include_router(preview_router)
app.include_router(_api)


# ── Health ────────────────────────────────────────────────────────────────────

@app.get("/", tags=["Health"])
async def root():
    return {
        "service": "PhilVerify",
        "version": "0.1.0",
        "status": "operational",
        "docs": "/docs",
    }


# Cloud Run health check (no /api prefix so load balancer can reach it)
# HEAD support for UptimeRobot and other uptime monitors
@app.get("/health", tags=["Health"])
@app.head("/health", tags=["Health"])
@app.get("/api/health", tags=["Health"])
@app.head("/api/health", tags=["Health"])
async def health():
    return {"status": "ok", "env": settings.app_env}


# ── Dev runner ────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=int(os.getenv("PORT", 8000)),
        reload=settings.debug,
        log_level=settings.log_level.lower(),
    )