""" PsyAdGenesis - FastAPI Application Design ads that stop the scroll. Generate high-converting ad creatives for Home Insurance and GLP-1 niches. Saves all ads to Neon PostgreSQL database with image URLs. """ import sys from pathlib import Path # Ensure project root is on path (fixes ModuleNotFoundError when run from /app or other cwd) _root = Path(__file__).resolve().parent if str(_root) not in sys.path: sys.path.insert(0, str(_root)) import os from contextlib import asynccontextmanager import httpx from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import Response as FastAPIResponse from starlette.middleware.gzip import GZipMiddleware from starlette.requests import Request as StarletteRequest from services.database import db_service from config import settings from api.routers import get_all_routers # Configure logging for API import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) @asynccontextmanager async def lifespan(app: FastAPI): """Startup and shutdown events.""" print("Starting PsyAdGenesis...") await db_service.connect() yield print("Shutting down...") await db_service.disconnect() app = FastAPI( title="PsyAdGenesis", description="Design ads that stop the scroll. Generate high-converting ad creatives using psychological triggers and AI-powered image generation.", version="2.0.0", lifespan=lifespan, ) # Middleware app.add_middleware(GZipMiddleware, minimum_size=1000) cors_origins = [ "http://localhost:3000", "http://127.0.0.1:3000", ] if os.getenv("CORS_ORIGINS"): cors_origins.extend([o.strip() for o in os.getenv("CORS_ORIGINS").split(",")]) app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_origin_regex=r"https://.*\.hf\.space", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.middleware("http") async def add_cache_headers(request: Request, call_next): response = await call_next(request) if request.url.path.startswith("/images/"): response.headers["Cache-Control"] = "public, max-age=31536000, immutable" return response # Static files os.makedirs(settings.output_dir, exist_ok=True) app.mount("/images", StaticFiles(directory=settings.output_dir), name="images") frontend_static_path = os.path.join(os.path.dirname(__file__), "frontend", ".next", "static") if os.path.exists(frontend_static_path): app.mount("/_next/static", StaticFiles(directory=frontend_static_path), name="nextjs_static") # Include all API routers for router in get_all_routers(): app.include_router(router) # Frontend proxy - must be last so it doesn't intercept API routes @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) async def frontend_proxy(path: str, request: StarletteRequest): """ Proxy frontend requests to Next.js server. Smart routing based on path AND HTTP method. """ api_only_routes = [ "auth/login", "api/correct", "api/download-image", "api/export/bulk", "db/stats", "db/ads", "strategies", "extensive/generate", "extensive/status", "extensive/result", ] api_post_routes = [ "generate", "generate/batch", "matrix/generate", "matrix/testing", ] api_get_routes = [ "matrix/angles", "matrix/concepts", "matrix/angle", "matrix/concept", "matrix/compatible", "db/ad", ] api_post_routes_additional = ["db/ad/edit"] if any(path == route or path.startswith(f"{route}/") for route in api_only_routes): raise HTTPException(status_code=404, detail="API endpoint not found") if request.method == "POST" and any(path == route or path.startswith(f"{route}/") for route in api_post_routes): raise HTTPException(status_code=404, detail="API endpoint not found") if request.method == "POST" and any(path == route or path.startswith(f"{route}/") for route in api_post_routes_additional): raise HTTPException(status_code=404, detail="API endpoint not found") if path.startswith("image/") or path.startswith("images/"): raise HTTPException(status_code=404, detail="API endpoint not found") if path.startswith("_next/static/"): raise HTTPException(status_code=404, detail="Static file not found") if request.method == "GET": for route in api_get_routes: if path == route or (path.startswith(f"{route}/") and "/" not in path[len(route) + 1:]): raise HTTPException(status_code=404, detail="API endpoint not found") try: async with httpx.AsyncClient(timeout=30.0) as client: nextjs_url = f"http://localhost:3000/{path}" if request.url.query: nextjs_url += f"?{request.url.query}" response = await client.request( method=request.method, url=nextjs_url, headers={k: v for k, v in request.headers.items() if k.lower() not in ["host", "content-length"]}, content=await request.body() if request.method in ["POST", "PUT", "PATCH"] else None, follow_redirects=True, ) return FastAPIResponse( content=response.content, status_code=response.status_code, headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-encoding", "transfer-encoding", "content-length"]}, media_type=response.headers.get("content-type"), ) except httpx.RequestError: raise HTTPException( status_code=503, detail="Frontend server is not available. Please ensure Next.js is running on port 3000.", ) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)