maryjscout commited on
Commit
949cc2a
Β·
verified Β·
1 Parent(s): 298e60b

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile.txt +56 -0
  2. ai_service.py +54 -0
  3. download_model.py +41 -0
  4. face_service.py +58 -0
  5. main.py +349 -0
  6. railway.toml +12 -0
  7. requirements.txt +9 -0
  8. start.sh +19 -0
Dockerfile.txt ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ──────────────────────────────────────────────────────────────────────────────
2
+ # Stage 1 β€” dependency builder
3
+ # Compiles packages that need build-essential (e.g. insightface C extensions).
4
+ # Keeps the final image lean by not shipping compilers to production.
5
+ # ──────────────────────────────────────────────────────────────────────────────
6
+ FROM python:3.11-slim AS builder
7
+
8
+ WORKDIR /app
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential \
12
+ libglib2.0-0 \
13
+ libgl1 \
14
+ libgomp1 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ COPY requirements.txt .
18
+
19
+ # Install into /install so we can copy just the packages to the final stage
20
+ RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
21
+
22
+
23
+ # ──────────────────────────────────────────────────────────────────────────────
24
+ # Stage 2 β€” runtime image (no compilers, smaller attack surface)
25
+ # ──────────────────────────────────────────────────────────────────────────────
26
+ FROM python:3.11-slim
27
+
28
+ WORKDIR /app
29
+
30
+ # Runtime system libs only
31
+ RUN apt-get update && apt-get install -y --no-install-recommends \
32
+ libglib2.0-0 \
33
+ libgl1 \
34
+ libgomp1 \
35
+ wget \
36
+ && rm -rf /var/lib/apt/lists/*
37
+
38
+ # Copy pre-built Python packages from the builder stage
39
+ COPY --from=builder /install /usr/local
40
+
41
+ # Copy application source
42
+ COPY . .
43
+
44
+ # ── PORT fix ──────────────────────────────────────────────────────────────────
45
+ # NEVER use JSON exec-form with $PORT:
46
+ # CMD ["uvicorn", "main:app", "--port", "$PORT"] ← shell never runs, $PORT
47
+ # is passed literally
48
+ #
49
+ # Instead, use start.sh which runs in a real shell AFTER Railway injects $PORT.
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ COPY start.sh /start.sh
52
+ RUN chmod +x /start.sh
53
+
54
+ EXPOSE 8000
55
+
56
+ CMD ["/start.sh"]
ai_service.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ai_service.py
3
+ =============
4
+ Google GenAI wrapper.
5
+ Supports both SDKs so the app works regardless of which is installed:
6
+ - google-genai (new unified SDK) β†’ preferred
7
+ - google-generativeai (legacy SDK) β†’ fallback
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ # Try new SDK first
14
+ try:
15
+ from google import genai as _genai_new
16
+ _NEW_SDK = True
17
+ except ImportError:
18
+ _NEW_SDK = False
19
+
20
+ # Try legacy SDK as fallback
21
+ try:
22
+ import google.generativeai as _genai_legacy
23
+ _LEGACY_SDK = True
24
+ except ImportError:
25
+ _LEGACY_SDK = False
26
+
27
+
28
+ def generate_ai_response(prompt: str) -> str:
29
+ """Return a Gemini text response, or a safe error string (never raises)."""
30
+
31
+ if not _NEW_SDK and not _LEGACY_SDK:
32
+ return "AI not available (neither google-genai nor google-generativeai is installed)."
33
+
34
+ api_key = os.getenv("GEMINI_API_KEY")
35
+ if not api_key:
36
+ return "AI not configured (GEMINI_API_KEY env var is missing)."
37
+
38
+ try:
39
+ if _NEW_SDK:
40
+ # New unified SDK (google-genai)
41
+ client = _genai_new.Client(api_key=api_key)
42
+ response = client.models.generate_content(
43
+ model="gemini-1.5-flash",
44
+ contents=prompt,
45
+ )
46
+ return response.text
47
+ else:
48
+ # Legacy SDK (google-generativeai)
49
+ _genai_legacy.configure(api_key=api_key)
50
+ model = _genai_legacy.GenerativeModel("gemini-1.5-flash")
51
+ return model.generate_content(prompt).text
52
+
53
+ except Exception as exc:
54
+ return f"AI error: {exc}"
download_model.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Download inswapper_128.onnx model for face swapping.
3
+ Run this once before starting the server: python download_model.py
4
+ """
5
+
6
+ import os
7
+ import urllib.request
8
+ from pathlib import Path
9
+
10
+ MODEL_DIR = Path(__file__).parent / "models"
11
+ MODEL_DIR.mkdir(exist_ok=True)
12
+
13
+ MODEL_PATH = MODEL_DIR / "inswapper_128.onnx"
14
+ MODEL_URL = "https://huggingface.co/kaizma/face-swap-inswapper/resolve/main/inswapper_128.onnx"
15
+
16
+ def download():
17
+ if MODEL_PATH.exists():
18
+ print(f"βœ… Model already exists at {MODEL_PATH}")
19
+ return
20
+
21
+ print(f"Downloading inswapper_128.onnx (~500MB)...")
22
+ print(f"From: {MODEL_URL}")
23
+ print(f"To: {MODEL_PATH}")
24
+ print("")
25
+
26
+ def progress(count, block_size, total_size):
27
+ pct = count * block_size * 100 // total_size
28
+ print(f"\r Progress: {pct}%", end="", flush=True)
29
+
30
+ try:
31
+ urllib.request.urlretrieve(MODEL_URL, MODEL_PATH, reporthook=progress)
32
+ print(f"\nβœ… Download complete!")
33
+ except Exception as e:
34
+ print(f"\n❌ Download failed: {e}")
35
+ print("\nManual download:")
36
+ print(f" 1. Go to: https://huggingface.co/deepinsight/inswapper")
37
+ print(f" 2. Download inswapper_128.onnx")
38
+ print(f" 3. Place it in: {MODEL_DIR}")
39
+
40
+ if __name__ == "__main__":
41
+ download()
face_service.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ face_service.py
3
+ ===============
4
+ InsightFace wrapper β€” lazy loading + guarded import.
5
+
6
+ The analyser is created on first call to get_analyser(), not at module import.
7
+ This means the server starts instantly even if the buffalo_l model pack
8
+ hasn't been downloaded yet.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ _face_analyser = None
13
+
14
+ # Guard: don't crash at import time if insightface isn't installed
15
+ try:
16
+ import insightface # noqa: F401 β€” availability check only
17
+ _AVAILABLE = True
18
+ except ImportError:
19
+ _AVAILABLE = False
20
+
21
+
22
+ def get_analyser():
23
+ """
24
+ Return the FaceAnalysis instance, initialising it on first call.
25
+ Thread-safety note: fine for single-worker Railway deployments.
26
+ """
27
+ global _face_analyser
28
+
29
+ if not _AVAILABLE:
30
+ raise RuntimeError(
31
+ "insightface is not installed. "
32
+ "Add it to requirements.txt and redeploy."
33
+ )
34
+
35
+ if _face_analyser is None:
36
+ from insightface.app import FaceAnalysis
37
+ print("⏳ Initialising FaceAnalysis (buffalo_l)…", flush=True)
38
+ _face_analyser = FaceAnalysis(
39
+ name="buffalo_l",
40
+ providers=["CPUExecutionProvider"],
41
+ )
42
+ _face_analyser.prepare(ctx_id=0, det_size=(640, 640))
43
+ print("βœ… FaceAnalysis ready", flush=True)
44
+
45
+ return _face_analyser
46
+
47
+
48
+ def process_face(data: dict) -> dict:
49
+ """
50
+ Face detection stub β€” replace the body with your real processing logic.
51
+ Returns a safe error dict instead of raising, so /face never returns 500.
52
+ """
53
+ try:
54
+ analyser = get_analyser()
55
+ # TODO: decode image from data["image_b64"], run analyser.get(img), etc.
56
+ return {"status": "ready", "faces_detected": "processing"}
57
+ except Exception as exc:
58
+ return {"error": str(exc)}
main.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Face Swap + Gemini API
3
+ ======================
4
+ Merged from python-api/main.py + python-api/python-api/main.py
5
+ All Railway deployment fixes applied.
6
+
7
+ Endpoints
8
+ ─────────
9
+ GET / β†’ health check
10
+ GET /health β†’ health check (alias)
11
+ POST /swap β†’ InsightFace face-swap (lazy-loaded, won't block startup)
12
+ POST /gemini/generate β†’ Gemini multimodal generation (legacy SDK)
13
+ POST /ai β†’ Gemini text generation (new SDK, from ai_service)
14
+ POST /face β†’ face detection stub (from face_service)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import base64
20
+ import os
21
+ import sys
22
+ import gc
23
+
24
+ # Silence chatty libraries BEFORE imports
25
+ os.environ["ORT_LOGGING_LEVEL"] = "3"
26
+ os.environ["KMP_WARNINGS"] = "0"
27
+ os.environ["INSIGHTFACE_HOME"] = "/tmp/models"
28
+ os.environ["PYTHONUNBUFFERED"] = "1"
29
+
30
+ import traceback
31
+ import logging
32
+ import warnings
33
+ from pathlib import Path
34
+ from typing import Optional
35
+
36
+ logging.getLogger("onnxruntime").setLevel(logging.ERROR)
37
+ logging.getLogger("insightface").setLevel(logging.ERROR)
38
+ warnings.filterwarnings("ignore")
39
+
40
+ # print(">>> NEW VERSION DEPLOYED - Python AI API starting up", flush=True)
41
+
42
+ import numpy as np
43
+ from fastapi import FastAPI, HTTPException, Request
44
+ from fastapi.middleware.cors import CORSMiddleware
45
+ from fastapi.responses import JSONResponse
46
+ from pydantic import BaseModel
47
+
48
+ # Service modules (your original files, also fixed)
49
+ from ai_service import generate_ai_response
50
+ from face_service import process_face
51
+
52
+ # ── App ───────────────────────────────────────────────────────────────────────
53
+
54
+ app = FastAPI(title="Face Swap + Gemini API", version="2.0.0")
55
+
56
+ # Allow requests from any origin (Netlify, localhost, etc.)
57
+ app.add_middleware(
58
+ CORSMiddleware,
59
+ allow_origins=["*"],
60
+ allow_credentials=True,
61
+ allow_methods=["*"],
62
+ allow_headers=["*"],
63
+ )
64
+
65
+ # Global exception handler β€” always return JSON, never bare HTML 500 pages
66
+ @app.exception_handler(Exception)
67
+ async def _global_exc_handler(request: Request, exc: Exception) -> JSONResponse:
68
+ # traceback.print_exc(file=sys.stderr)
69
+ return JSONResponse(
70
+ status_code=500,
71
+ content={"error": str(exc) or "Internal server error"},
72
+ )
73
+
74
+
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
+ # InsightFace β€” optional, guarded import
77
+ # ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ MODEL_DIR = Path(__file__).parent / "models"
80
+ SWAPPER_PATH = MODEL_DIR / "inswapper_128.onnx"
81
+
82
+ # Module-level handles β€” populated at startup
83
+ _face_analyser = None
84
+ _face_swapper = None
85
+ _celeb_cache: dict[str, object] = {}
86
+
87
+ try:
88
+ import cv2 # noqa: F401 β€” import check only
89
+ import insightface # noqa: F401
90
+ _INSIGHTFACE_AVAILABLE = True
91
+
92
+ from insightface.app import FaceAnalysis
93
+ from insightface.model_zoo import get_model
94
+
95
+ # print("⏳ Loading FaceAnalysis (buffalo_sc) once globally...", flush=True)
96
+ _face_analyser = FaceAnalysis(
97
+ name='buffalo_sc',
98
+ providers=["CPUExecutionProvider"]
99
+ )
100
+ _face_analyser.prepare(
101
+ ctx_id=-1,
102
+ det_size=(192, 192)
103
+ )
104
+ # print("βœ… FaceAnalysis ready", flush=True)
105
+
106
+ except ImportError:
107
+ _INSIGHTFACE_AVAILABLE = False
108
+ print("⚠️ insightface/cv2 not installed β€” /swap will return 503", file=sys.stderr)
109
+ except Exception as e:
110
+ print(f"❌ Error loading models at startup: {e}", flush=True)
111
+
112
+
113
+ def _load_inswapper():
114
+ """Lazily load inswapper model if not already loaded."""
115
+ global _face_swapper
116
+ if _face_swapper is not None:
117
+ return _face_swapper
118
+
119
+ from insightface.model_zoo import get_model
120
+ swapper_path = "/app/models/inswapper_128.onnx"
121
+ if not os.path.exists(swapper_path):
122
+ swapper_path = str(MODEL_DIR / "inswapper_128.onnx")
123
+
124
+ # print(f"⏳ Loading inswapper lazily from: {swapper_path}", flush=True)
125
+ _face_swapper = get_model(
126
+ swapper_path,
127
+ providers=["CPUExecutionProvider"]
128
+ )
129
+ # if _face_swapper:
130
+ # print("βœ… Inswapper ready", flush=True)
131
+ # else:
132
+ # print("❌ Inswapper failed to load (returned None)", flush=True)
133
+ # raise HTTPException(500, "Failed to load inswapper model")
134
+
135
+ return _face_swapper
136
+
137
+
138
+ # ── Image helpers (your original code, unchanged) ─────────────────────────────
139
+
140
+ def b64_to_img(b64: str) -> "np.ndarray":
141
+ import cv2
142
+ if "," in b64:
143
+ b64 = b64.split(",", 1)[1]
144
+ data = base64.b64decode(b64)
145
+ arr = np.frombuffer(data, np.uint8)
146
+ img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
147
+ if img is None:
148
+ raise ValueError("Could not decode image")
149
+
150
+ # Optimization: shrink incoming images to prevent OOM
151
+ h, w = img.shape[:2]
152
+ max_size = 720
153
+ if max(h, w) > max_size:
154
+ scale = max_size / max(h, w)
155
+ img = cv2.resize(
156
+ img,
157
+ (int(w * scale), int(h * scale))
158
+ )
159
+
160
+ return img
161
+
162
+
163
+ def img_to_b64(img: "np.ndarray", quality: int = 85) -> str:
164
+ import cv2
165
+ _, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, quality])
166
+ return base64.b64encode(buf).decode()
167
+
168
+
169
+ def best_face(analyser, img: "np.ndarray"):
170
+ faces = analyser.get(img)
171
+ if not faces:
172
+ return None
173
+ return max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
174
+
175
+
176
+ # ── Pydantic models ───────────────────────────────────────────────────────────
177
+
178
+ class SwapRequest(BaseModel):
179
+ source_key: str
180
+ source_b64: Optional[str] = None
181
+ target_b64: str
182
+ quality: int = 55
183
+
184
+
185
+ class AIRequest(BaseModel):
186
+ prompt: str
187
+
188
+
189
+ class GeminiPart(BaseModel):
190
+ text: Optional[str] = None
191
+ inlineData: Optional[dict] = None
192
+
193
+
194
+ class GeminiRequest(BaseModel):
195
+ systemInstruction: str
196
+ parts: list[GeminiPart]
197
+
198
+
199
+ # ─────────────────────────────────────────────────────────────────────────────
200
+ # Routes
201
+ # ─────────────────────────────────────────────────────────────────────────────
202
+
203
+ # ── Health ────────────────────────────────────────────────────────────────────
204
+
205
+ @app.get("/")
206
+ @app.get("/health")
207
+ def health():
208
+ face_models_in_memory = _face_analyser is not None and _face_swapper is not None
209
+ return {
210
+ "status": "ok",
211
+ # NOTE: models are lazy-loaded on first /swap POST, so this is False
212
+ # until the first swap request. We separately expose server_ready=True
213
+ # so the frontend can distinguish "server up but models not yet warm"
214
+ # from "server unreachable".
215
+ "models_loaded": face_models_in_memory,
216
+ # Always True while the server is running β€” frontend uses this for the
217
+ # reachability check instead of models_loaded.
218
+ "server_ready": True,
219
+ "insightface_available": _INSIGHTFACE_AVAILABLE,
220
+ "cached_faces": list(_celeb_cache.keys()),
221
+ "gemini_configured": bool(os.getenv("GEMINI_API_KEY")),
222
+ }
223
+
224
+
225
+ @app.get("/debug")
226
+ def debug():
227
+ return {
228
+ "models_folder_exists": os.path.exists("/app/models"),
229
+ "models": os.listdir("/app/models") if os.path.exists("/app/models") else "not found",
230
+ "inswapper_exists": os.path.exists("/app/models/inswapper_128.onnx")
231
+ }
232
+
233
+
234
+ # ── Face swap (your original logic, now loaded globally) ─────────────────────
235
+
236
+ @app.post("/swap")
237
+ async def swap(req: SwapRequest):
238
+ global _face_analyser
239
+ if _face_analyser is None:
240
+ raise HTTPException(503, "FaceAnalysis model is not loaded on this server.")
241
+
242
+ # Lazy load swapper
243
+ swapper = _load_inswapper()
244
+
245
+ # 1. Cache celebrity face if not already done
246
+ if req.source_key not in _celeb_cache:
247
+ if not req.source_b64:
248
+ raise HTTPException(400, "source_b64 required for first call with this key")
249
+ src_img = b64_to_img(req.source_b64)
250
+ face = best_face(_face_analyser, src_img)
251
+ if face is None:
252
+ raise HTTPException(422, "No face detected in source image")
253
+ _celeb_cache[req.source_key] = face
254
+
255
+ source_face = _celeb_cache[req.source_key]
256
+
257
+ # 2. Decode target frame
258
+ if not req.target_b64:
259
+ raise HTTPException(400, "target_b64 required")
260
+ target_img = b64_to_img(req.target_b64)
261
+ target_faces = _face_analyser.get(target_img)
262
+
263
+ # 3. No face in target β†’ return original unchanged
264
+ if not target_faces:
265
+ return {"swapped": False, "frame": img_to_b64(target_img, req.quality)}
266
+
267
+ # 4. Swap the largest detected face
268
+ target_face = max(
269
+ target_faces,
270
+ key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]),
271
+ )
272
+ result = swapper.get(target_img, target_face, source_face, paste_back=True)
273
+
274
+ # Free memory after heavy processing
275
+ gc.collect()
276
+
277
+ return {"swapped": True, "frame": img_to_b64(result, req.quality)}
278
+
279
+
280
+ # ── Gemini multimodal (your original /gemini/generate endpoint, fixed) ────────
281
+
282
+ @app.post("/gemini/generate")
283
+ async def generate_gemini(req: GeminiRequest):
284
+ # Guard: package must be installed
285
+ try:
286
+ import google.generativeai as genai
287
+ except ImportError:
288
+ raise HTTPException(503, "google-generativeai is not installed.")
289
+
290
+ api_key = os.getenv("GEMINI_API_KEY")
291
+ if not api_key:
292
+ raise HTTPException(500, "GEMINI_API_KEY is not configured on Railway.")
293
+
294
+ genai.configure(api_key=api_key)
295
+
296
+ # FIX: gemini-3.1-pro-preview does not exist β€” use env var or safe default
297
+ model_name = os.getenv("GEMINI_MODEL", "gemini-1.5-pro")
298
+ model = genai.GenerativeModel(model_name)
299
+
300
+ try:
301
+ # Build content parts (your original logic, unchanged)
302
+ from google.generativeai import types
303
+ content_parts = []
304
+ for p in req.parts:
305
+ if p.text:
306
+ content_parts.append(types.Part(text=p.text))
307
+ elif p.inlineData:
308
+ m_type = p.inlineData.get("mimeType") or p.inlineData.get("mime_type")
309
+ m_data = p.inlineData.get("data")
310
+ if m_type and m_data:
311
+ content_parts.append(
312
+ types.Part(
313
+ inline_data=types.Blob(
314
+ mime_type=m_type,
315
+ data=m_data,
316
+ )
317
+ )
318
+ )
319
+
320
+ response = model.generate_content(
321
+ contents=content_parts,
322
+ generation_config=genai.GenerationConfig(temperature=0.1),
323
+ system_instruction=req.systemInstruction,
324
+ )
325
+ return {"text": getattr(response, "text", str(response))}
326
+
327
+ except Exception as exc:
328
+ raise HTTPException(500, str(exc)) from exc
329
+
330
+
331
+ # ── Simple AI text endpoint (from your original python-api/main.py) ───────────
332
+
333
+ @app.post("/ai")
334
+ def ai(req: AIRequest):
335
+ """Thin wrapper around ai_service.generate_ai_response."""
336
+ return {"result": generate_ai_response(req.prompt)}
337
+
338
+
339
+ # ── Face stub endpoint (from your original python-api/main.py) ───────────────
340
+ @app.post("/face")
341
+ def face(data: dict):
342
+ """Thin wrapper around face_service.process_face."""
343
+ return {"result": process_face(data)}
344
+
345
+
346
+ if __name__ == "__main__":
347
+ import uvicorn
348
+ port = int(os.environ.get("PORT", 7860))
349
+ uvicorn.run(app, host="0.0.0.0", port=port, access_log=False)
railway.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build]
2
+ builder = "dockerfile"
3
+ dockerfilePath = "Dockerfile.txt"
4
+
5
+ [deploy]
6
+ # startCommand is intentionally NOT set here.
7
+ # Railway will use the CMD from the Dockerfile (start.sh).
8
+ # Setting startCommand would override start.sh and re-introduce the $PORT bug.
9
+ restartPolicyType = "on_failure"
10
+ restartPolicyMaxRetries = 3
11
+ healthcheckPath = "/health"
12
+ healthcheckTimeout = 30
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ google-genai
4
+ insightface
5
+ onnxruntime
6
+ opencv-python-headless
7
+ numpy
8
+ python-multipart
9
+ google-generativeai
start.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ PORT="${PORT:-8080}"
5
+
6
+ mkdir -p models
7
+
8
+ if [ ! -f models/inswapper_128.onnx ]; then
9
+ wget -q -O models/inswapper_128.onnx \
10
+ "https://huggingface.co/kaizma/face-swap-inswapper/resolve/main/inswapper_128.onnx"
11
+ fi
12
+
13
+ exec uvicorn main:app \
14
+ --host 0.0.0.0 \
15
+ --port "${PORT}" \
16
+ --workers 1 \
17
+ --no-access-log \
18
+ --log-level warning \
19
+ --timeout-keep-alive 30