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