Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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")), | |
| } | |
| 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) βββββββββββββββββββββ | |
| 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) ββββββββ | |
| 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) βββββββββββ | |
| 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) βββββββββββββββ | |
| 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) | |