File size: 6,011 Bytes
f201243
b8b7791
d4a4da7
 
f201243
 
62b339b
 
 
 
 
 
 
 
d4a4da7
f201243
d4a4da7
 
f201243
 
d4a4da7
f201243
b7334a4
f201243
 
 
 
d4a4da7
 
 
7906542
d4a4da7
7906542
 
d4a4da7
 
7906542
 
f201243
 
 
 
b8b7791
f201243
 
 
 
 
 
 
b8b7791
d4a4da7
f201243
 
 
 
d4a4da7
f201243
 
 
 
 
 
 
d4a4da7
f201243
 
 
 
d4a4da7
f201243
 
 
 
 
d4a4da7
f201243
 
 
 
 
 
 
d4a4da7
 
f201243
 
 
45ef06a
 
 
 
f201243
d4a4da7
 
 
f201243
 
d4a4da7
b7334a4
 
f201243
b7334a4
7551720
f201243
7551720
d4a4da7
 
7551720
 
d4a4da7
7551720
 
d4a4da7
 
addcf34
d4a4da7
 
7551720
b7334a4
7551720
 
addcf34
 
7551720
 
45ef06a
 
7551720
 
d4a4da7
7551720
d4a4da7
b7334a4
 
 
 
 
 
 
 
 
 
 
 
45ef06a
 
b7334a4
d4a4da7
b7334a4
 
 
 
 
d4a4da7
b7334a4
f201243
 
 
 
 
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""
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)