new66 / main.py
maryjscout's picture
Upload 11 files
6fca2d8 verified
Raw
History Blame Contribute Delete
12.4 kB
"""
Face Swap + Gemini API
======================
Merged from python-api/main.py + python-api/python-api/main.py
All Railway deployment fixes applied.
Endpoints
─────────
GET / β†’ health check
GET /health β†’ health check (alias)
POST /swap β†’ InsightFace face-swap (lazy-loaded, won't block startup)
POST /gemini/generate β†’ Gemini multimodal generation (legacy SDK)
POST /ai β†’ Gemini text generation (new SDK, from ai_service)
POST /face β†’ face detection stub (from face_service)
"""
from __future__ import annotations
import base64
import os
import sys
import gc
# Silence chatty libraries BEFORE imports
os.environ["ORT_LOGGING_LEVEL"] = "3"
os.environ["KMP_WARNINGS"] = "0"
os.environ["INSIGHTFACE_HOME"] = "/app/models"
os.environ["PYTHONUNBUFFERED"] = "1"
import traceback
import logging
import warnings
from pathlib import Path
from typing import Optional
logging.getLogger("onnxruntime").setLevel(logging.ERROR)
logging.getLogger("insightface").setLevel(logging.ERROR)
warnings.filterwarnings("ignore")
# print(">>> NEW VERSION DEPLOYED - Python AI API starting up", flush=True)
import numpy as np
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
# Service modules (your original files, also fixed)
from ai_service import generate_ai_response
from face_service import process_face
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(title="Face Swap + Gemini API", version="2.0.0")
# Allow requests from any origin (Netlify, localhost, etc.)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global exception handler β€” always return JSON, never bare HTML 500 pages
@app.exception_handler(Exception)
async def _global_exc_handler(request: Request, exc: Exception) -> JSONResponse:
# traceback.print_exc(file=sys.stderr)
return JSONResponse(
status_code=500,
content={"error": str(exc) or "Internal server error"},
)
# ─────────────────────────────────────────────────────────────────────────────
# InsightFace β€” optional, guarded import
# ─────────────────────────────────────────────────────────────────────────────
MODEL_DIR = Path(__file__).parent / "models"
SWAPPER_PATH = MODEL_DIR / "inswapper_128.onnx"
# Module-level handles β€” populated at startup
_face_analyser = None
_face_swapper = None
_celeb_cache: dict[str, object] = {}
try:
import cv2 # noqa: F401 β€” import check only
import insightface # noqa: F401
_INSIGHTFACE_AVAILABLE = True
from insightface.app import FaceAnalysis
from insightface.model_zoo import get_model
# print("⏳ Loading FaceAnalysis (buffalo_sc) once globally...", flush=True)
_face_analyser = FaceAnalysis(
name="buffalo_sc",
root=".",
providers=["CPUExecutionProvider"]
)
_face_analyser.prepare(
ctx_id=-1,
det_size=(192, 192)
)
# print("βœ… FaceAnalysis ready", flush=True)
except ImportError:
_INSIGHTFACE_AVAILABLE = False
print("⚠️ insightface/cv2 not installed β€” /swap will return 503", file=sys.stderr)
except Exception as e:
print(f"❌ Error loading models at startup: {e}", flush=True)
def _load_inswapper():
"""Lazily load inswapper model if not already loaded."""
global _face_swapper
if _face_swapper is not None:
return _face_swapper
from insightface.model_zoo import get_model
swapper_path = "./models/inswapper_128.onnx"
if not os.path.exists(swapper_path):
swapper_path = "/app/models/inswapper_128.onnx"
if not os.path.exists(swapper_path):
swapper_path = str(MODEL_DIR / "inswapper_128.onnx")
# print(f"⏳ Loading inswapper lazily from: {swapper_path}", flush=True)
_face_swapper = get_model(
swapper_path,
download=False,
providers=["CPUExecutionProvider"]
)
return _face_swapper
# ── Image helpers (your original code, unchanged) ─────────────────────────────
def b64_to_img(b64: str) -> "np.ndarray":
import cv2
if "," in b64:
b64 = b64.split(",", 1)[1]
data = base64.b64decode(b64)
arr = np.frombuffer(data, np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if img is None:
raise ValueError("Could not decode image")
# Optimization: shrink incoming images to prevent OOM
h, w = img.shape[:2]
max_size = 720
if max(h, w) > max_size:
scale = max_size / max(h, w)
img = cv2.resize(
img,
(int(w * scale), int(h * scale))
)
return img
def img_to_b64(img: "np.ndarray", quality: int = 85) -> str:
import cv2
_, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, quality])
return base64.b64encode(buf).decode()
def best_face(analyser, img: "np.ndarray"):
faces = analyser.get(img)
if not faces:
return None
return max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
# ── Pydantic models ───────────────────────────────────────────────────────────
class SwapRequest(BaseModel):
source_key: str
source_b64: Optional[str] = None
target_b64: str
quality: int = 55
class AIRequest(BaseModel):
prompt: str
class GeminiPart(BaseModel):
text: Optional[str] = None
inlineData: Optional[dict] = None
class GeminiRequest(BaseModel):
systemInstruction: str
parts: list[GeminiPart]
# ─────────────────────────────────────────────────────────────────────────────
# Routes
# ─────────────────────────────────────────────────────────────────────────────
# ── Health ────────────────────────────────────────────────────────────────────
@app.get("/")
@app.get("/health")
def health():
face_models_in_memory = _face_analyser is not None and _face_swapper is not None
return {
"status": "ok",
# NOTE: models are lazy-loaded on first /swap POST, so this is False
# until the first swap request. We separately expose server_ready=True
# so the frontend can distinguish "server up but models not yet warm"
# from "server unreachable".
"models_loaded": face_models_in_memory,
# Always True while the server is running β€” frontend uses this for the
# reachability check instead of models_loaded.
"server_ready": True,
"insightface_available": _INSIGHTFACE_AVAILABLE,
"cached_faces": list(_celeb_cache.keys()),
"gemini_configured": bool(os.getenv("GEMINI_API_KEY")),
}
@app.get("/debug")
def debug():
return {
"models_folder_exists": os.path.exists("/app/models"),
"models": os.listdir("/app/models") if os.path.exists("/app/models") else "not found",
"inswapper_exists": os.path.exists("/app/models/inswapper_128.onnx")
}
# ── Face swap (your original logic, now loaded globally) ─────────────────────
@app.post("/swap")
async def swap(req: SwapRequest):
global _face_analyser
if _face_analyser is None:
raise HTTPException(503, "FaceAnalysis model is not loaded on this server.")
# Lazy load swapper
swapper = _load_inswapper()
# 1. Cache celebrity face if not already done
if req.source_key not in _celeb_cache:
if not req.source_b64:
raise HTTPException(400, "source_b64 required for first call with this key")
src_img = b64_to_img(req.source_b64)
face = best_face(_face_analyser, src_img)
if face is None:
raise HTTPException(422, "No face detected in source image")
_celeb_cache[req.source_key] = face
source_face = _celeb_cache[req.source_key]
# 2. Decode target frame
if not req.target_b64:
raise HTTPException(400, "target_b64 required")
target_img = b64_to_img(req.target_b64)
target_faces = _face_analyser.get(target_img)
# 3. No face in target β†’ return original unchanged
if not target_faces:
return {"swapped": False, "frame": img_to_b64(target_img, req.quality)}
# 4. Swap the largest detected face
target_face = max(
target_faces,
key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]),
)
result = swapper.get(target_img, target_face, source_face, paste_back=True)
# Free memory after heavy processing
gc.collect()
return {"swapped": True, "frame": img_to_b64(result, req.quality)}
# ── Gemini multimodal (your original /gemini/generate endpoint, fixed) ────────
@app.post("/gemini/generate")
async def generate_gemini(req: GeminiRequest):
# Guard: package must be installed
try:
import google.generativeai as genai
except ImportError:
raise HTTPException(503, "google-generativeai is not installed.")
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
raise HTTPException(500, "GEMINI_API_KEY is not configured on Railway.")
genai.configure(api_key=api_key)
# FIX: gemini-3.1-pro-preview does not exist β€” use env var or safe default
model_name = os.getenv("GEMINI_MODEL", "gemini-1.5-pro")
model = genai.GenerativeModel(model_name)
try:
# Build content parts (your original logic, unchanged)
from google.generativeai import types
content_parts = []
for p in req.parts:
if p.text:
content_parts.append(types.Part(text=p.text))
elif p.inlineData:
m_type = p.inlineData.get("mimeType") or p.inlineData.get("mime_type")
m_data = p.inlineData.get("data")
if m_type and m_data:
content_parts.append(
types.Part(
inline_data=types.Blob(
mime_type=m_type,
data=m_data,
)
)
)
response = model.generate_content(
contents=content_parts,
generation_config=genai.GenerationConfig(temperature=0.1),
system_instruction=req.systemInstruction,
)
return {"text": getattr(response, "text", str(response))}
except Exception as exc:
raise HTTPException(500, str(exc)) from exc
# ── Simple AI text endpoint (from your original python-api/main.py) ───────────
@app.post("/ai")
def ai(req: AIRequest):
"""Thin wrapper around ai_service.generate_ai_response."""
return {"result": generate_ai_response(req.prompt)}
# ── Face stub endpoint (from your original python-api/main.py) ───────────────
@app.post("/face")
def face(data: dict):
"""Thin wrapper around face_service.process_face."""
return {"result": process_face(data)}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port, access_log=False)