LogicGoInfotechSpaces commited on
Commit
1179404
·
verified ·
1 Parent(s): cc4e1a8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +633 -151
app.py CHANGED
@@ -9,20 +9,16 @@ import threading
9
  import subprocess
10
  import logging
11
  from datetime import datetime, timezone
12
-
13
  import insightface
14
  from insightface.app import FaceAnalysis
15
  from huggingface_hub import hf_hub_download
16
-
17
  from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
18
  from fastapi.responses import RedirectResponse
19
  from pydantic import BaseModel
20
  from motor.motor_asyncio import AsyncIOMotorClient
21
-
22
  import uvicorn
23
  import gradio as gr
24
  from gradio import mount_gradio_app
25
-
26
  # DigitalOcean Spaces
27
  import boto3
28
  from botocore.client import Config
@@ -31,40 +27,32 @@ from typing import Optional
31
  import requests
32
  import json
33
  from bson import ObjectId
34
-
35
  # --------------------- Logging ---------------------
36
  logging.basicConfig(level=logging.INFO)
37
  logger = logging.getLogger(__name__)
38
-
39
  # --------------------- Paths -----------------------
40
  REPO_ID = "HariLogicgo/face_swap_models"
41
  BASE_DIR = "./workspace"
42
  MODELS_DIR = "./models"
43
-
44
  os.makedirs(MODELS_DIR, exist_ok=True)
45
-
46
  # --------------------- Secrets ---------------------
47
  HF_TOKEN = os.getenv("HF_TOKEN") # Hugging Face private repo token
48
  # Firebase credentials JSON
49
  FIREBASE_CREDENTIALS_PATH = os.getenv("FIREBASE_CREDENTIALS_PATH")
50
-
51
  # --------------------- DigitalOcean Spaces Credentials ---------------------
52
  DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
53
  DO_SPACES_ENDPOINT = os.getenv("DO_SPACES_ENDPOINT", f"https://{DO_SPACES_REGION}.digitaloceanspaces.com")
54
  DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
55
  DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
56
  DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
57
-
58
  # --------------------- Firebase Auth ---------------------
59
  import firebase_admin
60
  from firebase_admin import credentials, auth
61
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
62
-
63
  if not firebase_admin._apps:
64
  FIREBASE_CREDENTIALS = os.getenv("FIREBASE_CREDENTIALS_PATH")
65
  if not FIREBASE_CREDENTIALS:
66
  raise RuntimeError("❌ FIREBASE_CREDENTIALS_PATH not set in environment variables")
67
-
68
  try:
69
  # Try parsing as JSON string
70
  cred_dict = json.loads(FIREBASE_CREDENTIALS)
@@ -74,11 +62,8 @@ if not firebase_admin._apps:
74
  # Fallback: assume it's a file path
75
  cred = credentials.Certificate(FIREBASE_CREDENTIALS)
76
  logger.info("✅ Firebase initialized from JSON file path")
77
-
78
  firebase_admin.initialize_app(cred)
79
-
80
  security = HTTPBearer()
81
-
82
  def verify_firebase_token(credentials: HTTPAuthorizationCredentials = Security(security)):
83
  """Verify Firebase ID token from Authorization header."""
84
  try:
@@ -91,7 +76,6 @@ def verify_firebase_token(credentials: HTTPAuthorizationCredentials = Security(s
91
  except Exception as e:
92
  logger.error(f"Firebase auth failed: {e}")
93
  raise HTTPException(status_code=401, detail="Unauthorized: Invalid Firebase token")
94
-
95
  # --------------------- Download Models ---------------------
96
  def download_models():
97
  logger.info("Downloading models from private HF repo...")
@@ -102,7 +86,6 @@ def download_models():
102
  local_dir=MODELS_DIR,
103
  token=HF_TOKEN
104
  )
105
-
106
  buffalo_files = [
107
  "1k3d68.onnx",
108
  "2d106det.onnx",
@@ -120,18 +103,14 @@ def download_models():
120
  )
121
  logger.info("Models downloaded successfully")
122
  return inswapper_path
123
-
124
  inswapper_path = download_models()
125
-
126
  # --------------------- Face Analysis + Swapper ---------------------
127
  providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
128
  face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
129
  face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
130
  swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
131
-
132
  # --------------------- CodeFormer ---------------------
133
  CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
134
-
135
  def ensure_codeformer():
136
  if not os.path.exists("CodeFormer"):
137
  subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
@@ -139,30 +118,21 @@ def ensure_codeformer():
139
  subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
140
  subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=True)
141
  subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=True)
142
-
143
  ensure_codeformer()
144
-
145
  # --------------------- MongoDB ---------------------
146
  MONGODB_URL = os.getenv("MONGODB_URL")
147
-
148
  client = None
149
  database = None
150
-
151
- # --------------------- Admin Panel DB (categories + media_clicks) ---------------------
152
  # --------------------- Admin Panel DB (categories + subcategories + media_clicks) ---------------------
153
  ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
154
  admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
155
  admin_db = admin_client.adminPanel
156
-
157
  # Collections
158
  categories_col = admin_db.categories
159
  subcategories_col = admin_db.subcategories
160
  media_clicks_col = admin_db.media_clicks
161
- users_col = admin_db.users # optional, only if needed
162
-
163
  # --------------------- FastAPI ---------------------
164
  fastapi_app = FastAPI()
165
-
166
  @fastapi_app.on_event("startup")
167
  async def startup_db():
168
  global client, database
@@ -170,22 +140,18 @@ async def startup_db():
170
  client = AsyncIOMotorClient(MONGODB_URL)
171
  database = client.FaceSwap
172
  logger.info("MongoDB initialized for API logs")
173
-
174
  @fastapi_app.on_event("shutdown")
175
  async def shutdown_db():
176
  global client
177
  if client:
178
  client.close()
179
  logger.info("MongoDB connection closed")
180
-
181
  # --------------------- Logging API Hits ---------------------
182
  async def log_faceswap_hit(user_email: str, status: str, start_time: datetime, end_time: datetime):
183
  global database
184
  if database is None:
185
  return
186
-
187
  response_time_ms = (end_time - start_time).total_seconds() * 1000
188
-
189
  await database.api_logs.insert_one({
190
  "user": user_email,
191
  "endpoint": "/face-swap",
@@ -195,73 +161,109 @@ async def log_faceswap_hit(user_email: str, status: str, start_time: datetime, e
195
  "response_time_ms": response_time_ms
196
  })
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  # --------------------- Face Swap Pipeline ---------------------
200
  swap_lock = threading.Lock()
201
-
202
  def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
203
  try:
204
  with swap_lock:
205
  if os.path.exists(temp_dir):
206
  shutil.rmtree(temp_dir)
207
  os.makedirs(temp_dir, exist_ok=True)
208
-
209
  src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
210
  tgt_bgr_full = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
211
-
212
  src_faces = face_analysis_app.get(src_bgr)
213
  tgt_faces = face_analysis_app.get(tgt_bgr_full)
214
-
215
  if not src_faces or not tgt_faces:
216
  return None, None, "❌ Face not detected in source or target image"
217
-
218
  src_face0 = src_faces[0]
219
  tgt_face0 = tgt_faces[0]
220
-
221
  swapped_bgr_full = swapper.get(tgt_bgr_full, tgt_face0, src_face0)
222
  if swapped_bgr_full is None:
223
  return None, None, "❌ Face swap failed"
224
-
225
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
226
  cv2.imwrite(swapped_path, swapped_bgr_full)
227
-
228
  cmd = f"python {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
229
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
230
  if result.returncode != 0:
231
  return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
232
-
233
  final_results_dir = os.path.join(temp_dir, "final_results")
234
  final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
235
  if not final_files:
236
  return None, None, "❌ No enhanced image found"
237
-
238
  final_path = os.path.join(final_results_dir, final_files[0])
239
  final_img = cv2.cvtColor(cv2.imread(final_path), cv2.COLOR_BGR2RGB)
240
-
241
  return final_img, final_path, ""
242
-
243
  except Exception as e:
244
  return None, None, f"❌ Error: {str(e)}"
245
-
246
  # --------------------- Gradio ---------------------
247
  with gr.Blocks() as demo:
248
  gr.Markdown("Face Swap")
249
-
250
  with gr.Row():
251
  src_input = gr.Image(type="numpy", label="Upload Your Face")
252
  tgt_input = gr.Image(type="numpy", label="Upload Target Image")
253
-
254
  btn = gr.Button("Swap Face")
255
  output_img = gr.Image(type="numpy", label="Enhanced Output")
256
  download = gr.File(label="⬇️ Download Enhanced Image")
257
  error_box = gr.Textbox(label="Logs / Errors", interactive=False)
258
-
259
  def process(src, tgt):
260
  img, path, err = face_swap_and_enhance(src, tgt)
261
  return img, path, err
262
-
263
  btn.click(process, [src_input, tgt_input], [output_img, download, error_box])
264
-
265
  # --------------------- DigitalOcean Spaces Helper ---------------------
266
  def get_spaces_client():
267
  session = boto3.session.Session()
@@ -274,22 +276,18 @@ def get_spaces_client():
274
  config=Config(signature_version='s3v4')
275
  )
276
  return client
277
-
278
  def upload_to_spaces(file_bytes, key, content_type="image/png"):
279
  client = get_spaces_client()
280
  client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
281
  return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
282
-
283
  def download_from_spaces(key):
284
  client = get_spaces_client()
285
  obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
286
  return obj['Body'].read()
287
-
288
  # --------------------- API Endpoints ---------------------
289
  @fastapi_app.get("/")
290
  def root():
291
  return RedirectResponse("/gradio")
292
-
293
  @fastapi_app.get("/health")
294
  async def health():
295
  return {"status": "healthy"}
@@ -297,40 +295,36 @@ async def health():
297
  @fastapi_app.post("/face-swap")
298
  async def face_swap_api(
299
  source: UploadFile = File(...),
300
- target_category_id: str = Form(...), # REQUIRED (old behavior preserved)
301
  category_id: Optional[str] = Form(None),
302
  user_id: Optional[str] = Form(None),
303
  new_subcategory_id: Optional[str] = Form(None),
304
  user_email: str = Depends(verify_firebase_token)
305
  ):
306
  start_time = datetime.now(timezone.utc)
307
-
 
308
  try:
309
  # ---------------------------------------------------------
310
  # NORMALIZE EMPTY STRINGS (Android older versions)
311
  # ---------------------------------------------------------
312
- if target_category_id == "":
313
- target_category_id = None
314
-
315
- if new_subcategory_id == "":
316
- new_subcategory_id = None
317
-
318
- if category_id == "":
319
- category_id = None
320
-
321
- if user_id == "":
322
- user_id = None
323
-
324
  # ---------------------------------------------------------
325
  # STRICT XOR VALIDATION
326
  # ---------------------------------------------------------
327
- if target_category_id and new_subcategory_id:
 
 
 
328
  raise HTTPException(
329
  status_code=400,
330
  detail="Provide ONLY ONE of: target_category_id OR new_subcategory_id"
331
  )
332
-
333
- if not target_category_id and not new_subcategory_id:
334
  raise HTTPException(
335
  status_code=400,
336
  detail="Either target_category_id OR new_subcategory_id is required"
@@ -344,57 +338,71 @@ async def face_swap_api(
344
  upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
345
 
346
  # ---------------------------------------------------------
347
- # CASE 1 Old behavior (use DO Spaces target image)
348
  # ---------------------------------------------------------
349
- if target_category_id:
 
 
350
  target_filename = f"{target_category_id}.png"
351
  target_url = (
352
  f"https://{DO_SPACES_BUCKET}.{DO_SPACES_REGION}."
353
  f"digitaloceanspaces.com/bikini-theme/target/{target_filename}"
354
  )
355
-
 
 
 
 
356
  resp = requests.get(target_url)
357
  if resp.status_code != 200:
358
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
359
  raise HTTPException(status_code=404, detail=f"Target image not found: {target_url}")
360
-
361
  tgt_bytes = resp.content
362
 
363
- # ---------------------------------------------------------
364
  # CASE 2 — New behavior (use subcategory asset image)
365
- # ---------------------------------------------------------
366
- else:
367
- # Find subcategory asset by asset_images._id
368
- asset = await admin_db.subcategories.find_one(
369
- {"asset_images._id": ObjectId(new_subcategory_id)},
370
- {"asset_images.$": 1}
 
 
 
 
 
371
  )
372
 
373
- if not asset or "asset_images" not in asset:
374
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
375
  raise HTTPException(
376
  status_code=404,
377
- detail="Subcategory asset image not found"
378
  )
379
-
380
- # Extract the single matching image URL
381
- asset_url = asset["asset_images"][0]["url"]
382
-
 
 
 
 
 
 
383
  resp = requests.get(asset_url)
384
  if resp.status_code != 200:
 
385
  raise HTTPException(
386
  status_code=404,
387
  detail=f"Failed to download asset image: {asset_url}"
388
  )
389
-
390
  tgt_bytes = resp.content
391
 
392
  # ---------------------------------------------------------
393
- # DECODE BOTH IMAGES
394
  # ---------------------------------------------------------
395
  src_array = np.frombuffer(src_bytes, np.uint8)
396
  tgt_array = np.frombuffer(tgt_bytes, np.uint8)
397
-
398
  src_bgr = cv2.imdecode(src_array, cv2.IMREAD_COLOR)
399
  tgt_bgr = cv2.imdecode(tgt_array, cv2.IMREAD_COLOR)
400
 
@@ -402,6 +410,7 @@ async def face_swap_api(
402
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
403
  raise HTTPException(status_code=400, detail="Invalid image data")
404
 
 
405
  src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
406
  tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
407
 
@@ -416,111 +425,584 @@ async def face_swap_api(
416
  # Save final output to DO Spaces
417
  with open(final_path, "rb") as f:
418
  result_bytes = f.read()
419
-
420
  result_key = f"bikini-theme/result/{uuid.uuid4().hex}_enhanced.png"
421
  result_url = upload_to_spaces(result_bytes, result_key, "image/png")
422
-
423
- await log_faceswap_hit(user_email, "success", start_time, datetime.now(timezone.utc))
424
-
425
  # ---------------------------------------------------------
426
  # SUCCESS RESPONSE
427
  # ---------------------------------------------------------
 
 
428
  return {
429
  "result_url": result_url,
430
- "category_id": category_id,
431
  "user_id": user_id,
432
  "new_subcategory_id": new_subcategory_id
433
  }
434
 
 
 
 
435
  except Exception as e:
436
- await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
437
- raise HTTPException(status_code=500, detail=f"Face swap failed: {str(e)}")
 
 
 
 
 
 
 
438
 
439
 
440
- ####------------------------------------OLD CODE------------------------------------####
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  # @fastapi_app.post("/face-swap")
442
  # async def face_swap_api(
443
  # source: UploadFile = File(...),
444
- # target_category_id: str = Form(...),
 
 
 
445
  # user_email: str = Depends(verify_firebase_token)
446
  # ):
447
- # # start_time = datetime.utcnow()
448
  # start_time = datetime.now(timezone.utc)
 
449
  # try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  # src_bytes = await source.read()
451
  # src_key = f"bikini-theme/source/{uuid.uuid4().hex}_{source.filename}"
452
  # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
453
 
454
- # target_filename = f"{target_category_id}.png"
455
- # target_url = f"https://{DO_SPACES_BUCKET}.{DO_SPACES_REGION}.digitaloceanspaces.com/bikini-theme/target/{target_filename}"
456
-
457
- # resp = requests.get(target_url)
458
- # if resp.status_code != 200:
459
- # # end_time = datetime.utcnow()
460
- # end_time = datetime.now(timezone.utc)
461
- # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
462
- # raise HTTPException(status_code=404, detail=f"Target image not found at {target_url}")
463
-
464
- # tgt_bytes = resp.content
465
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  # src_array = np.frombuffer(src_bytes, np.uint8)
467
  # tgt_array = np.frombuffer(tgt_bytes, np.uint8)
 
468
  # src_bgr = cv2.imdecode(src_array, cv2.IMREAD_COLOR)
469
  # tgt_bgr = cv2.imdecode(tgt_array, cv2.IMREAD_COLOR)
470
 
471
  # if src_bgr is None or tgt_bgr is None:
472
- # #end_time = datetime.utcnow()
473
- # end_time = datetime.now(timezone.utc)
474
-
475
- # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
476
  # raise HTTPException(status_code=400, detail="Invalid image data")
477
 
478
  # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
479
  # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
480
 
 
 
 
481
  # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
482
  # if err:
483
- # end_time = datetime.utcnow()
484
- # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
485
  # raise HTTPException(status_code=500, detail=err)
486
 
 
487
  # with open(final_path, "rb") as f:
488
  # result_bytes = f.read()
 
489
  # result_key = f"bikini-theme/result/{uuid.uuid4().hex}_enhanced.png"
490
- # result_url = upload_to_spaces(result_bytes, result_key, content_type="image/png")
491
 
492
- # #end_time = datetime.utcnow()
493
- # end_time = datetime.now(timezone.utc)
494
- # await log_faceswap_hit(user_email, status="success", start_time=start_time, end_time=end_time)
495
 
496
- # return {"result_url": result_url}
 
 
 
 
 
 
 
 
497
 
498
  # except Exception as e:
499
- # #end_time = datetime.utcnow()
500
- # end_time = datetime.now(timezone.utc)
501
-
502
- # # Ensure we log the error with timestamps before raising
503
- # try:
504
- # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
505
- # except Exception as log_exc:
506
- # logger.error("Failed to write log_faceswap_hit: %s", log_exc)
507
  # raise HTTPException(status_code=500, detail=f"Face swap failed: {str(e)}")
508
 
509
 
510
- @fastapi_app.get("/preview/{result_key:path}")
511
- async def preview_result(result_key: str):
512
- try:
513
- img_bytes = download_from_spaces(result_key)
514
- except Exception:
515
- raise HTTPException(status_code=404, detail="Result not found")
516
- return Response(
517
- content=img_bytes,
518
- media_type="image/png",
519
- headers={"Content-Disposition": "inline; filename=result.png"}
520
- )
521
-
522
- # --------------------- Mount Gradio ---------------------
523
- fastapi_app = mount_gradio_app(fastapi_app, demo, path="/gradio")
524
-
525
- if __name__ == "__main__":
526
- uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  import subprocess
10
  import logging
11
  from datetime import datetime, timezone
 
12
  import insightface
13
  from insightface.app import FaceAnalysis
14
  from huggingface_hub import hf_hub_download
 
15
  from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
16
  from fastapi.responses import RedirectResponse
17
  from pydantic import BaseModel
18
  from motor.motor_asyncio import AsyncIOMotorClient
 
19
  import uvicorn
20
  import gradio as gr
21
  from gradio import mount_gradio_app
 
22
  # DigitalOcean Spaces
23
  import boto3
24
  from botocore.client import Config
 
27
  import requests
28
  import json
29
  from bson import ObjectId
 
30
  # --------------------- Logging ---------------------
31
  logging.basicConfig(level=logging.INFO)
32
  logger = logging.getLogger(__name__)
 
33
  # --------------------- Paths -----------------------
34
  REPO_ID = "HariLogicgo/face_swap_models"
35
  BASE_DIR = "./workspace"
36
  MODELS_DIR = "./models"
 
37
  os.makedirs(MODELS_DIR, exist_ok=True)
 
38
  # --------------------- Secrets ---------------------
39
  HF_TOKEN = os.getenv("HF_TOKEN") # Hugging Face private repo token
40
  # Firebase credentials JSON
41
  FIREBASE_CREDENTIALS_PATH = os.getenv("FIREBASE_CREDENTIALS_PATH")
 
42
  # --------------------- DigitalOcean Spaces Credentials ---------------------
43
  DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
44
  DO_SPACES_ENDPOINT = os.getenv("DO_SPACES_ENDPOINT", f"https://{DO_SPACES_REGION}.digitaloceanspaces.com")
45
  DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
46
  DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
47
  DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
 
48
  # --------------------- Firebase Auth ---------------------
49
  import firebase_admin
50
  from firebase_admin import credentials, auth
51
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 
52
  if not firebase_admin._apps:
53
  FIREBASE_CREDENTIALS = os.getenv("FIREBASE_CREDENTIALS_PATH")
54
  if not FIREBASE_CREDENTIALS:
55
  raise RuntimeError("❌ FIREBASE_CREDENTIALS_PATH not set in environment variables")
 
56
  try:
57
  # Try parsing as JSON string
58
  cred_dict = json.loads(FIREBASE_CREDENTIALS)
 
62
  # Fallback: assume it's a file path
63
  cred = credentials.Certificate(FIREBASE_CREDENTIALS)
64
  logger.info("✅ Firebase initialized from JSON file path")
 
65
  firebase_admin.initialize_app(cred)
 
66
  security = HTTPBearer()
 
67
  def verify_firebase_token(credentials: HTTPAuthorizationCredentials = Security(security)):
68
  """Verify Firebase ID token from Authorization header."""
69
  try:
 
76
  except Exception as e:
77
  logger.error(f"Firebase auth failed: {e}")
78
  raise HTTPException(status_code=401, detail="Unauthorized: Invalid Firebase token")
 
79
  # --------------------- Download Models ---------------------
80
  def download_models():
81
  logger.info("Downloading models from private HF repo...")
 
86
  local_dir=MODELS_DIR,
87
  token=HF_TOKEN
88
  )
 
89
  buffalo_files = [
90
  "1k3d68.onnx",
91
  "2d106det.onnx",
 
103
  )
104
  logger.info("Models downloaded successfully")
105
  return inswapper_path
 
106
  inswapper_path = download_models()
 
107
  # --------------------- Face Analysis + Swapper ---------------------
108
  providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
109
  face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
110
  face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
111
  swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
 
112
  # --------------------- CodeFormer ---------------------
113
  CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
 
114
  def ensure_codeformer():
115
  if not os.path.exists("CodeFormer"):
116
  subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
 
118
  subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
119
  subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=True)
120
  subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=True)
 
121
  ensure_codeformer()
 
122
  # --------------------- MongoDB ---------------------
123
  MONGODB_URL = os.getenv("MONGODB_URL")
 
124
  client = None
125
  database = None
 
 
126
  # --------------------- Admin Panel DB (categories + subcategories + media_clicks) ---------------------
127
  ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
128
  admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
129
  admin_db = admin_client.adminPanel
 
130
  # Collections
131
  categories_col = admin_db.categories
132
  subcategories_col = admin_db.subcategories
133
  media_clicks_col = admin_db.media_clicks
 
 
134
  # --------------------- FastAPI ---------------------
135
  fastapi_app = FastAPI()
 
136
  @fastapi_app.on_event("startup")
137
  async def startup_db():
138
  global client, database
 
140
  client = AsyncIOMotorClient(MONGODB_URL)
141
  database = client.FaceSwap
142
  logger.info("MongoDB initialized for API logs")
 
143
  @fastapi_app.on_event("shutdown")
144
  async def shutdown_db():
145
  global client
146
  if client:
147
  client.close()
148
  logger.info("MongoDB connection closed")
 
149
  # --------------------- Logging API Hits ---------------------
150
  async def log_faceswap_hit(user_email: str, status: str, start_time: datetime, end_time: datetime):
151
  global database
152
  if database is None:
153
  return
 
154
  response_time_ms = (end_time - start_time).total_seconds() * 1000
 
155
  await database.api_logs.insert_one({
156
  "user": user_email,
157
  "endpoint": "/face-swap",
 
161
  "response_time_ms": response_time_ms
162
  })
163
 
164
+ # --------------------- Media Click Logging Helper ---------------------
165
+ async def log_media_click(user_id: str, category_oid_str: str):
166
+ """
167
+ Logs a click event to the media_clicks collection against the Category.
168
+ """
169
+ try:
170
+ user_oid = ObjectId(user_id.strip())
171
+ category_oid = ObjectId(category_oid_str.strip())
172
+ now = datetime.utcnow()
173
+
174
+ # Check if category exists (optional, but prevents logging invalid IDs)
175
+ if not await categories_col.find_one({"_id": category_oid}):
176
+ logger.warning(f"Category ID {category_oid_str} not found. Skipping click logging.")
177
+ return
178
+
179
+ # 1) Try updating existing category click entry
180
+ update_result = await media_clicks_col.update_one(
181
+ {
182
+ "userId": user_oid,
183
+ "categories.categoryId": category_oid
184
+ },
185
+ {
186
+ "$set": {
187
+ "updatedAt": now,
188
+ "categories.$.lastClickedAt": now
189
+ },
190
+ "$inc": {
191
+ "categories.$.click_count": 1
192
+ }
193
+ }
194
+ )
195
+
196
+ # 2) If no category entry exists → push new one (or create doc)
197
+ if update_result.matched_count == 0:
198
+ await media_clicks_col.update_one(
199
+ { "userId": user_oid },
200
+ {
201
+ "$setOnInsert": { "createdAt": now },
202
+ "$set": { "updatedAt": now },
203
+ "$push": {
204
+ "categories": {
205
+ "categoryId": category_oid,
206
+ "click_count": 1,
207
+ "lastClickedAt": now
208
+ }
209
+ }
210
+ },
211
+ upsert=True
212
+ )
213
+
214
+ logger.info(f"Media click logged for User {user_id} on Category {category_oid_str}")
215
+
216
+ except Exception as media_err:
217
+ logger.error(f"MEDIA_CLICK LOGGING ERROR: {media_err}")
218
 
219
  # --------------------- Face Swap Pipeline ---------------------
220
  swap_lock = threading.Lock()
 
221
  def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
222
  try:
223
  with swap_lock:
224
  if os.path.exists(temp_dir):
225
  shutil.rmtree(temp_dir)
226
  os.makedirs(temp_dir, exist_ok=True)
 
227
  src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
228
  tgt_bgr_full = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
 
229
  src_faces = face_analysis_app.get(src_bgr)
230
  tgt_faces = face_analysis_app.get(tgt_bgr_full)
 
231
  if not src_faces or not tgt_faces:
232
  return None, None, "❌ Face not detected in source or target image"
 
233
  src_face0 = src_faces[0]
234
  tgt_face0 = tgt_faces[0]
 
235
  swapped_bgr_full = swapper.get(tgt_bgr_full, tgt_face0, src_face0)
236
  if swapped_bgr_full is None:
237
  return None, None, "❌ Face swap failed"
 
238
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
239
  cv2.imwrite(swapped_path, swapped_bgr_full)
 
240
  cmd = f"python {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
241
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
242
  if result.returncode != 0:
243
  return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
 
244
  final_results_dir = os.path.join(temp_dir, "final_results")
245
  final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
246
  if not final_files:
247
  return None, None, "❌ No enhanced image found"
 
248
  final_path = os.path.join(final_results_dir, final_files[0])
249
  final_img = cv2.cvtColor(cv2.imread(final_path), cv2.COLOR_BGR2RGB)
 
250
  return final_img, final_path, ""
 
251
  except Exception as e:
252
  return None, None, f"❌ Error: {str(e)}"
 
253
  # --------------------- Gradio ---------------------
254
  with gr.Blocks() as demo:
255
  gr.Markdown("Face Swap")
 
256
  with gr.Row():
257
  src_input = gr.Image(type="numpy", label="Upload Your Face")
258
  tgt_input = gr.Image(type="numpy", label="Upload Target Image")
 
259
  btn = gr.Button("Swap Face")
260
  output_img = gr.Image(type="numpy", label="Enhanced Output")
261
  download = gr.File(label="⬇️ Download Enhanced Image")
262
  error_box = gr.Textbox(label="Logs / Errors", interactive=False)
 
263
  def process(src, tgt):
264
  img, path, err = face_swap_and_enhance(src, tgt)
265
  return img, path, err
 
266
  btn.click(process, [src_input, tgt_input], [output_img, download, error_box])
 
267
  # --------------------- DigitalOcean Spaces Helper ---------------------
268
  def get_spaces_client():
269
  session = boto3.session.Session()
 
276
  config=Config(signature_version='s3v4')
277
  )
278
  return client
 
279
  def upload_to_spaces(file_bytes, key, content_type="image/png"):
280
  client = get_spaces_client()
281
  client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
282
  return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
 
283
  def download_from_spaces(key):
284
  client = get_spaces_client()
285
  obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
286
  return obj['Body'].read()
 
287
  # --------------------- API Endpoints ---------------------
288
  @fastapi_app.get("/")
289
  def root():
290
  return RedirectResponse("/gradio")
 
291
  @fastapi_app.get("/health")
292
  async def health():
293
  return {"status": "healthy"}
 
295
  @fastapi_app.post("/face-swap")
296
  async def face_swap_api(
297
  source: UploadFile = File(...),
298
+ target_category_id: Optional[str] = Form(None), # <--- NOW OPTIONAL
299
  category_id: Optional[str] = Form(None),
300
  user_id: Optional[str] = Form(None),
301
  new_subcategory_id: Optional[str] = Form(None),
302
  user_email: str = Depends(verify_firebase_token)
303
  ):
304
  start_time = datetime.now(timezone.utc)
305
+ target_url = None
306
+
307
  try:
308
  # ---------------------------------------------------------
309
  # NORMALIZE EMPTY STRINGS (Android older versions)
310
  # ---------------------------------------------------------
311
+ if target_category_id == "": target_category_id = None
312
+ if new_subcategory_id == "": new_subcategory_id = None
313
+ if category_id == "": category_id = None
314
+ if user_id == "": user_id = None
315
+
 
 
 
 
 
 
 
316
  # ---------------------------------------------------------
317
  # STRICT XOR VALIDATION
318
  # ---------------------------------------------------------
319
+ target_provided = target_category_id is not None
320
+ new_sub_provided = new_subcategory_id is not None
321
+
322
+ if target_provided and new_sub_provided:
323
  raise HTTPException(
324
  status_code=400,
325
  detail="Provide ONLY ONE of: target_category_id OR new_subcategory_id"
326
  )
327
+ if not target_provided and not new_sub_provided:
 
328
  raise HTTPException(
329
  status_code=400,
330
  detail="Either target_category_id OR new_subcategory_id is required"
 
338
  upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
339
 
340
  # ---------------------------------------------------------
341
+ # TARGET IMAGE RETRIEVAL & LOGGING
342
  # ---------------------------------------------------------
343
+
344
+ # CASE 1 — Old behavior (use DO Spaces target image)
345
+ if target_provided:
346
  target_filename = f"{target_category_id}.png"
347
  target_url = (
348
  f"https://{DO_SPACES_BUCKET}.{DO_SPACES_REGION}."
349
  f"digitaloceanspaces.com/bikini-theme/target/{target_filename}"
350
  )
351
+
352
+ # Log click for old system (if user_id and category_id are passed)
353
+ if user_id and category_id:
354
+ await log_media_click(user_id, category_id)
355
+
356
  resp = requests.get(target_url)
357
  if resp.status_code != 200:
358
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
359
  raise HTTPException(status_code=404, detail=f"Target image not found: {target_url}")
 
360
  tgt_bytes = resp.content
361
 
 
362
  # CASE 2 — New behavior (use subcategory asset image)
363
+ elif new_sub_provided:
364
+
365
+ try:
366
+ asset_oid = ObjectId(new_subcategory_id)
367
+ except:
368
+ raise HTTPException(400, "Invalid new_subcategory_id format.")
369
+
370
+ # 1. Find subcategory asset by asset_images._id
371
+ subcat_doc = await subcategories_col.find_one(
372
+ {"asset_images._id": asset_oid},
373
+ {"asset_images.$": 1, "categoryId": 1} # Need categoryId for logging
374
  )
375
 
376
+ if not subcat_doc or "asset_images" not in subcat_doc or not subcat_doc.get("categoryId"):
377
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
378
  raise HTTPException(
379
  status_code=404,
380
+ detail="Subcategory asset or parent category ID not found in DB."
381
  )
382
+
383
+ asset_url = subcat_doc["asset_images"][0]["url"]
384
+ target_url = asset_url
385
+ parent_category_id = str(subcat_doc["categoryId"]) # Category ID for logging
386
+
387
+ # Log click for new system (uses parent category ID from the subcategory doc)
388
+ if user_id:
389
+ await log_media_click(user_id, parent_category_id)
390
+
391
+ # 2. Download target image
392
  resp = requests.get(asset_url)
393
  if resp.status_code != 200:
394
+ await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
395
  raise HTTPException(
396
  status_code=404,
397
  detail=f"Failed to download asset image: {asset_url}"
398
  )
 
399
  tgt_bytes = resp.content
400
 
401
  # ---------------------------------------------------------
402
+ # DECODE & FACE SWAP (The rest of the logic remains the same)
403
  # ---------------------------------------------------------
404
  src_array = np.frombuffer(src_bytes, np.uint8)
405
  tgt_array = np.frombuffer(tgt_bytes, np.uint8)
 
406
  src_bgr = cv2.imdecode(src_array, cv2.IMREAD_COLOR)
407
  tgt_bgr = cv2.imdecode(tgt_array, cv2.IMREAD_COLOR)
408
 
 
410
  await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
411
  raise HTTPException(status_code=400, detail="Invalid image data")
412
 
413
+ # Convert to RGB for processing
414
  src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
415
  tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
416
 
 
425
  # Save final output to DO Spaces
426
  with open(final_path, "rb") as f:
427
  result_bytes = f.read()
 
428
  result_key = f"bikini-theme/result/{uuid.uuid4().hex}_enhanced.png"
429
  result_url = upload_to_spaces(result_bytes, result_key, "image/png")
430
+
 
 
431
  # ---------------------------------------------------------
432
  # SUCCESS RESPONSE
433
  # ---------------------------------------------------------
434
+ await log_faceswap_hit(user_email, "success", start_time, datetime.now(timezone.utc))
435
+
436
  return {
437
  "result_url": result_url,
438
+ "category_id": category_id,
439
  "user_id": user_id,
440
  "new_subcategory_id": new_subcategory_id
441
  }
442
 
443
+ except HTTPException:
444
+ raise
445
+
446
  except Exception as e:
447
+ end_time = datetime.now(timezone.utc)
448
+ try:
449
+ await log_faceswap_hit(user_email, "error", start_time, end_time)
450
+ except Exception as log_exc:
451
+ logger.error("Failed to write log_faceswap_hit: %s", log_exc)
452
+
453
+ logger.error(f"Critical /face-swap error: {e}")
454
+ # The HTTPException detail might include sensitive info like target_url, simplified for the final response
455
+ raise HTTPException(status_code=500, detail=f"Face swap failed: Internal server error.")
456
 
457
 
458
+ @fastapi_app.get("/preview/{result_key:path}")
459
+ async def preview_result(result_key: str):
460
+ try:
461
+ img_bytes = download_from_spaces(result_key)
462
+ except Exception:
463
+ raise HTTPException(status_code=404, detail="Result not found")
464
+ return Response(
465
+ content=img_bytes,
466
+ media_type="image/png",
467
+ headers={"Content-Disposition": "inline; filename=result.png"}
468
+ )
469
+ # --------------------- Mount Gradio ---------------------
470
+ fastapi_app = mount_gradio_app(fastapi_app, demo, path="/gradio")
471
+ if __name__ == "__main__":
472
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
473
+
474
+
475
+
476
+
477
+
478
+
479
+
480
+
481
+
482
+
483
+ # # --------------------- List Images Endpoint ---------------------
484
+ # import os
485
+ # os.environ["OMP_NUM_THREADS"] = "1"
486
+ # import shutil
487
+ # import uuid
488
+ # import cv2
489
+ # import numpy as np
490
+ # import threading
491
+ # import subprocess
492
+ # import logging
493
+ # from datetime import datetime, timezone
494
+
495
+ # import insightface
496
+ # from insightface.app import FaceAnalysis
497
+ # from huggingface_hub import hf_hub_download
498
+
499
+ # from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
500
+ # from fastapi.responses import RedirectResponse
501
+ # from pydantic import BaseModel
502
+ # from motor.motor_asyncio import AsyncIOMotorClient
503
+
504
+ # import uvicorn
505
+ # import gradio as gr
506
+ # from gradio import mount_gradio_app
507
+
508
+ # # DigitalOcean Spaces
509
+ # import boto3
510
+ # from botocore.client import Config
511
+ # from io import BytesIO
512
+ # from typing import Optional
513
+ # import requests
514
+ # import json
515
+ # from bson import ObjectId
516
+
517
+ # # --------------------- Logging ---------------------
518
+ # logging.basicConfig(level=logging.INFO)
519
+ # logger = logging.getLogger(__name__)
520
+
521
+ # # --------------------- Paths -----------------------
522
+ # REPO_ID = "HariLogicgo/face_swap_models"
523
+ # BASE_DIR = "./workspace"
524
+ # MODELS_DIR = "./models"
525
+
526
+ # os.makedirs(MODELS_DIR, exist_ok=True)
527
+
528
+ # # --------------------- Secrets ---------------------
529
+ # HF_TOKEN = os.getenv("HF_TOKEN") # Hugging Face private repo token
530
+ # # Firebase credentials JSON
531
+ # FIREBASE_CREDENTIALS_PATH = os.getenv("FIREBASE_CREDENTIALS_PATH")
532
+
533
+ # # --------------------- DigitalOcean Spaces Credentials ---------------------
534
+ # DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
535
+ # DO_SPACES_ENDPOINT = os.getenv("DO_SPACES_ENDPOINT", f"https://{DO_SPACES_REGION}.digitaloceanspaces.com")
536
+ # DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
537
+ # DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
538
+ # DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
539
+
540
+ # # --------------------- Firebase Auth ---------------------
541
+ # import firebase_admin
542
+ # from firebase_admin import credentials, auth
543
+ # from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
544
+
545
+ # if not firebase_admin._apps:
546
+ # FIREBASE_CREDENTIALS = os.getenv("FIREBASE_CREDENTIALS_PATH")
547
+ # if not FIREBASE_CREDENTIALS:
548
+ # raise RuntimeError("❌ FIREBASE_CREDENTIALS_PATH not set in environment variables")
549
+
550
+ # try:
551
+ # # Try parsing as JSON string
552
+ # cred_dict = json.loads(FIREBASE_CREDENTIALS)
553
+ # cred = credentials.Certificate(cred_dict)
554
+ # logger.info("✅ Firebase initialized from JSON string in environment variable")
555
+ # except json.JSONDecodeError:
556
+ # # Fallback: assume it's a file path
557
+ # cred = credentials.Certificate(FIREBASE_CREDENTIALS)
558
+ # logger.info("✅ Firebase initialized from JSON file path")
559
+
560
+ # firebase_admin.initialize_app(cred)
561
+
562
+ # security = HTTPBearer()
563
+
564
+ # def verify_firebase_token(credentials: HTTPAuthorizationCredentials = Security(security)):
565
+ # """Verify Firebase ID token from Authorization header."""
566
+ # try:
567
+ # id_token = credentials.credentials
568
+ # decoded_token = auth.verify_id_token(id_token)
569
+ # user_email = decoded_token.get("email")
570
+ # if not user_email:
571
+ # raise HTTPException(status_code=401, detail="Firebase token invalid or missing email")
572
+ # return user_email
573
+ # except Exception as e:
574
+ # logger.error(f"Firebase auth failed: {e}")
575
+ # raise HTTPException(status_code=401, detail="Unauthorized: Invalid Firebase token")
576
+
577
+ # # --------------------- Download Models ---------------------
578
+ # def download_models():
579
+ # logger.info("Downloading models from private HF repo...")
580
+ # inswapper_path = hf_hub_download(
581
+ # repo_id=REPO_ID,
582
+ # filename="models/inswapper_128.onnx",
583
+ # repo_type="model",
584
+ # local_dir=MODELS_DIR,
585
+ # token=HF_TOKEN
586
+ # )
587
+
588
+ # buffalo_files = [
589
+ # "1k3d68.onnx",
590
+ # "2d106det.onnx",
591
+ # "genderage.onnx",
592
+ # "det_10g.onnx",
593
+ # "w600k_r50.onnx"
594
+ # ]
595
+ # for f in buffalo_files:
596
+ # hf_hub_download(
597
+ # repo_id=REPO_ID,
598
+ # filename=f"models/buffalo_l/{f}",
599
+ # repo_type="model",
600
+ # local_dir=MODELS_DIR,
601
+ # token=HF_TOKEN
602
+ # )
603
+ # logger.info("Models downloaded successfully")
604
+ # return inswapper_path
605
+
606
+ # inswapper_path = download_models()
607
+
608
+ # # --------------------- Face Analysis + Swapper ---------------------
609
+ # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
610
+ # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
611
+ # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
612
+ # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
613
+
614
+ # # --------------------- CodeFormer ---------------------
615
+ # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
616
+
617
+ # def ensure_codeformer():
618
+ # if not os.path.exists("CodeFormer"):
619
+ # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
620
+ # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=True)
621
+ # subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
622
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=True)
623
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=True)
624
+
625
+ # ensure_codeformer()
626
+
627
+ # # --------------------- MongoDB ---------------------
628
+ # MONGODB_URL = os.getenv("MONGODB_URL")
629
+
630
+ # client = None
631
+ # database = None
632
+
633
+ # # --------------------- Admin Panel DB (categories + media_clicks) ---------------------
634
+ # # --------------------- Admin Panel DB (categories + subcategories + media_clicks) ---------------------
635
+ # ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
636
+ # admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
637
+ # admin_db = admin_client.adminPanel
638
+
639
+ # # Collections
640
+ # categories_col = admin_db.categories
641
+ # subcategories_col = admin_db.subcategories
642
+ # media_clicks_col = admin_db.media_clicks
643
+ # users_col = admin_db.users # optional, only if needed
644
+
645
+ # # --------------------- FastAPI ---------------------
646
+ # fastapi_app = FastAPI()
647
+
648
+ # @fastapi_app.on_event("startup")
649
+ # async def startup_db():
650
+ # global client, database
651
+ # logger.info("Initializing MongoDB for API logs...")
652
+ # client = AsyncIOMotorClient(MONGODB_URL)
653
+ # database = client.FaceSwap
654
+ # logger.info("MongoDB initialized for API logs")
655
+
656
+ # @fastapi_app.on_event("shutdown")
657
+ # async def shutdown_db():
658
+ # global client
659
+ # if client:
660
+ # client.close()
661
+ # logger.info("MongoDB connection closed")
662
+
663
+ # # --------------------- Logging API Hits ---------------------
664
+ # async def log_faceswap_hit(user_email: str, status: str, start_time: datetime, end_time: datetime):
665
+ # global database
666
+ # if database is None:
667
+ # return
668
+
669
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
670
+
671
+ # await database.api_logs.insert_one({
672
+ # "user": user_email,
673
+ # "endpoint": "/face-swap",
674
+ # "status": status,
675
+ # "start_time": start_time,
676
+ # "end_time": end_time,
677
+ # "response_time_ms": response_time_ms
678
+ # })
679
+
680
+
681
+ # # --------------------- Face Swap Pipeline ---------------------
682
+ # swap_lock = threading.Lock()
683
+
684
+ # def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
685
+ # try:
686
+ # with swap_lock:
687
+ # if os.path.exists(temp_dir):
688
+ # shutil.rmtree(temp_dir)
689
+ # os.makedirs(temp_dir, exist_ok=True)
690
+
691
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
692
+ # tgt_bgr_full = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
693
+
694
+ # src_faces = face_analysis_app.get(src_bgr)
695
+ # tgt_faces = face_analysis_app.get(tgt_bgr_full)
696
+
697
+ # if not src_faces or not tgt_faces:
698
+ # return None, None, "❌ Face not detected in source or target image"
699
+
700
+ # src_face0 = src_faces[0]
701
+ # tgt_face0 = tgt_faces[0]
702
+
703
+ # swapped_bgr_full = swapper.get(tgt_bgr_full, tgt_face0, src_face0)
704
+ # if swapped_bgr_full is None:
705
+ # return None, None, "❌ Face swap failed"
706
+
707
+ # swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
708
+ # cv2.imwrite(swapped_path, swapped_bgr_full)
709
+
710
+ # cmd = f"python {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
711
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
712
+ # if result.returncode != 0:
713
+ # return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
714
+
715
+ # final_results_dir = os.path.join(temp_dir, "final_results")
716
+ # final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
717
+ # if not final_files:
718
+ # return None, None, "❌ No enhanced image found"
719
+
720
+ # final_path = os.path.join(final_results_dir, final_files[0])
721
+ # final_img = cv2.cvtColor(cv2.imread(final_path), cv2.COLOR_BGR2RGB)
722
+
723
+ # return final_img, final_path, ""
724
+
725
+ # except Exception as e:
726
+ # return None, None, f"❌ Error: {str(e)}"
727
+
728
+ # # --------------------- Gradio ---------------------
729
+ # with gr.Blocks() as demo:
730
+ # gr.Markdown("Face Swap")
731
+
732
+ # with gr.Row():
733
+ # src_input = gr.Image(type="numpy", label="Upload Your Face")
734
+ # tgt_input = gr.Image(type="numpy", label="Upload Target Image")
735
+
736
+ # btn = gr.Button("Swap Face")
737
+ # output_img = gr.Image(type="numpy", label="Enhanced Output")
738
+ # download = gr.File(label="⬇️ Download Enhanced Image")
739
+ # error_box = gr.Textbox(label="Logs / Errors", interactive=False)
740
+
741
+ # def process(src, tgt):
742
+ # img, path, err = face_swap_and_enhance(src, tgt)
743
+ # return img, path, err
744
+
745
+ # btn.click(process, [src_input, tgt_input], [output_img, download, error_box])
746
+
747
+ # # --------------------- DigitalOcean Spaces Helper ---------------------
748
+ # def get_spaces_client():
749
+ # session = boto3.session.Session()
750
+ # client = session.client(
751
+ # 's3',
752
+ # region_name=DO_SPACES_REGION,
753
+ # endpoint_url=DO_SPACES_ENDPOINT,
754
+ # aws_access_key_id=DO_SPACES_KEY,
755
+ # aws_secret_access_key=DO_SPACES_SECRET,
756
+ # config=Config(signature_version='s3v4')
757
+ # )
758
+ # return client
759
+
760
+ # def upload_to_spaces(file_bytes, key, content_type="image/png"):
761
+ # client = get_spaces_client()
762
+ # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
763
+ # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
764
+
765
+ # def download_from_spaces(key):
766
+ # client = get_spaces_client()
767
+ # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
768
+ # return obj['Body'].read()
769
+
770
+ # # --------------------- API Endpoints ---------------------
771
+ # @fastapi_app.get("/")
772
+ # def root():
773
+ # return RedirectResponse("/gradio")
774
+
775
+ # @fastapi_app.get("/health")
776
+ # async def health():
777
+ # return {"status": "healthy"}
778
+
779
  # @fastapi_app.post("/face-swap")
780
  # async def face_swap_api(
781
  # source: UploadFile = File(...),
782
+ # target_category_id: str = Form(...), # REQUIRED (old behavior preserved)
783
+ # category_id: Optional[str] = Form(None),
784
+ # user_id: Optional[str] = Form(None),
785
+ # new_subcategory_id: Optional[str] = Form(None),
786
  # user_email: str = Depends(verify_firebase_token)
787
  # ):
 
788
  # start_time = datetime.now(timezone.utc)
789
+
790
  # try:
791
+ # # ---------------------------------------------------------
792
+ # # NORMALIZE EMPTY STRINGS (Android older versions)
793
+ # # ---------------------------------------------------------
794
+ # if target_category_id == "":
795
+ # target_category_id = None
796
+
797
+ # if new_subcategory_id == "":
798
+ # new_subcategory_id = None
799
+
800
+ # if category_id == "":
801
+ # category_id = None
802
+
803
+ # if user_id == "":
804
+ # user_id = None
805
+
806
+ # # ---------------------------------------------------------
807
+ # # STRICT XOR VALIDATION
808
+ # # ---------------------------------------------------------
809
+ # if target_category_id and new_subcategory_id:
810
+ # raise HTTPException(
811
+ # status_code=400,
812
+ # detail="Provide ONLY ONE of: target_category_id OR new_subcategory_id"
813
+ # )
814
+
815
+ # if not target_category_id and not new_subcategory_id:
816
+ # raise HTTPException(
817
+ # status_code=400,
818
+ # detail="Either target_category_id OR new_subcategory_id is required"
819
+ # )
820
+
821
+ # # ---------------------------------------------------------
822
+ # # READ SOURCE IMAGE
823
+ # # ---------------------------------------------------------
824
  # src_bytes = await source.read()
825
  # src_key = f"bikini-theme/source/{uuid.uuid4().hex}_{source.filename}"
826
  # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
827
 
828
+ # # ---------------------------------------------------------
829
+ # # CASE 1 — Old behavior (use DO Spaces target image)
830
+ # # ---------------------------------------------------------
831
+ # if target_category_id:
832
+ # target_filename = f"{target_category_id}.png"
833
+ # target_url = (
834
+ # f"https://{DO_SPACES_BUCKET}.{DO_SPACES_REGION}."
835
+ # f"digitaloceanspaces.com/bikini-theme/target/{target_filename}"
836
+ # )
837
+
838
+ # resp = requests.get(target_url)
839
+ # if resp.status_code != 200:
840
+ # await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
841
+ # raise HTTPException(status_code=404, detail=f"Target image not found: {target_url}")
842
+
843
+ # tgt_bytes = resp.content
844
+
845
+ # # ---------------------------------------------------------
846
+ # # CASE 2 — New behavior (use subcategory asset image)
847
+ # # ---------------------------------------------------------
848
+ # else:
849
+ # # Find subcategory asset by asset_images._id
850
+ # asset = await admin_db.subcategories.find_one(
851
+ # {"asset_images._id": ObjectId(new_subcategory_id)},
852
+ # {"asset_images.$": 1}
853
+ # )
854
+
855
+ # if not asset or "asset_images" not in asset:
856
+ # await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
857
+ # raise HTTPException(
858
+ # status_code=404,
859
+ # detail="Subcategory asset image not found"
860
+ # )
861
+
862
+ # # Extract the single matching image URL
863
+ # asset_url = asset["asset_images"][0]["url"]
864
+
865
+ # resp = requests.get(asset_url)
866
+ # if resp.status_code != 200:
867
+ # raise HTTPException(
868
+ # status_code=404,
869
+ # detail=f"Failed to download asset image: {asset_url}"
870
+ # )
871
+
872
+ # tgt_bytes = resp.content
873
+
874
+ # # ---------------------------------------------------------
875
+ # # DECODE BOTH IMAGES
876
+ # # ---------------------------------------------------------
877
  # src_array = np.frombuffer(src_bytes, np.uint8)
878
  # tgt_array = np.frombuffer(tgt_bytes, np.uint8)
879
+
880
  # src_bgr = cv2.imdecode(src_array, cv2.IMREAD_COLOR)
881
  # tgt_bgr = cv2.imdecode(tgt_array, cv2.IMREAD_COLOR)
882
 
883
  # if src_bgr is None or tgt_bgr is None:
884
+ # await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
 
 
 
885
  # raise HTTPException(status_code=400, detail="Invalid image data")
886
 
887
  # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
888
  # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
889
 
890
+ # # ---------------------------------------------------------
891
+ # # FACE SWAP & ENHANCE
892
+ # # ---------------------------------------------------------
893
  # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
894
  # if err:
895
+ # await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
 
896
  # raise HTTPException(status_code=500, detail=err)
897
 
898
+ # # Save final output to DO Spaces
899
  # with open(final_path, "rb") as f:
900
  # result_bytes = f.read()
901
+
902
  # result_key = f"bikini-theme/result/{uuid.uuid4().hex}_enhanced.png"
903
+ # result_url = upload_to_spaces(result_bytes, result_key, "image/png")
904
 
905
+ # await log_faceswap_hit(user_email, "success", start_time, datetime.now(timezone.utc))
 
 
906
 
907
+ # # ---------------------------------------------------------
908
+ # # SUCCESS RESPONSE
909
+ # # ---------------------------------------------------------
910
+ # return {
911
+ # "result_url": result_url,
912
+ # "category_id": category_id,
913
+ # "user_id": user_id,
914
+ # "new_subcategory_id": new_subcategory_id
915
+ # }
916
 
917
  # except Exception as e:
918
+ # await log_faceswap_hit(user_email, "error", start_time, datetime.now(timezone.utc))
 
 
 
 
 
 
 
919
  # raise HTTPException(status_code=500, detail=f"Face swap failed: {str(e)}")
920
 
921
 
922
+ # ####------------------------------------OLD CODE------------------------------------####
923
+ # # @fastapi_app.post("/face-swap")
924
+ # # async def face_swap_api(
925
+ # # source: UploadFile = File(...),
926
+ # # target_category_id: str = Form(...),
927
+ # # user_email: str = Depends(verify_firebase_token)
928
+ # # ):
929
+ # # # start_time = datetime.utcnow()
930
+ # # start_time = datetime.now(timezone.utc)
931
+ # # try:
932
+ # # src_bytes = await source.read()
933
+ # # src_key = f"bikini-theme/source/{uuid.uuid4().hex}_{source.filename}"
934
+ # # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
935
+
936
+ # # target_filename = f"{target_category_id}.png"
937
+ # # target_url = f"https://{DO_SPACES_BUCKET}.{DO_SPACES_REGION}.digitaloceanspaces.com/bikini-theme/target/{target_filename}"
938
+
939
+ # # resp = requests.get(target_url)
940
+ # # if resp.status_code != 200:
941
+ # # # end_time = datetime.utcnow()
942
+ # # end_time = datetime.now(timezone.utc)
943
+ # # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
944
+ # # raise HTTPException(status_code=404, detail=f"Target image not found at {target_url}")
945
+
946
+ # # tgt_bytes = resp.content
947
+
948
+ # # src_array = np.frombuffer(src_bytes, np.uint8)
949
+ # # tgt_array = np.frombuffer(tgt_bytes, np.uint8)
950
+ # # src_bgr = cv2.imdecode(src_array, cv2.IMREAD_COLOR)
951
+ # # tgt_bgr = cv2.imdecode(tgt_array, cv2.IMREAD_COLOR)
952
+
953
+ # # if src_bgr is None or tgt_bgr is None:
954
+ # # #end_time = datetime.utcnow()
955
+ # # end_time = datetime.now(timezone.utc)
956
+
957
+ # # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
958
+ # # raise HTTPException(status_code=400, detail="Invalid image data")
959
+
960
+ # # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
961
+ # # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
962
+
963
+ # # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
964
+ # # if err:
965
+ # # end_time = datetime.utcnow()
966
+ # # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
967
+ # # raise HTTPException(status_code=500, detail=err)
968
+
969
+ # # with open(final_path, "rb") as f:
970
+ # # result_bytes = f.read()
971
+ # # result_key = f"bikini-theme/result/{uuid.uuid4().hex}_enhanced.png"
972
+ # # result_url = upload_to_spaces(result_bytes, result_key, content_type="image/png")
973
+
974
+ # # #end_time = datetime.utcnow()
975
+ # # end_time = datetime.now(timezone.utc)
976
+ # # await log_faceswap_hit(user_email, status="success", start_time=start_time, end_time=end_time)
977
+
978
+ # # return {"result_url": result_url}
979
+
980
+ # # except Exception as e:
981
+ # # #end_time = datetime.utcnow()
982
+ # # end_time = datetime.now(timezone.utc)
983
+
984
+ # # # Ensure we log the error with timestamps before raising
985
+ # # try:
986
+ # # await log_faceswap_hit(user_email, status="error", start_time=start_time, end_time=end_time)
987
+ # # except Exception as log_exc:
988
+ # # logger.error("Failed to write log_faceswap_hit: %s", log_exc)
989
+ # # raise HTTPException(status_code=500, detail=f"Face swap failed: {str(e)}")
990
+
991
+
992
+ # @fastapi_app.get("/preview/{result_key:path}")
993
+ # async def preview_result(result_key: str):
994
+ # try:
995
+ # img_bytes = download_from_spaces(result_key)
996
+ # except Exception:
997
+ # raise HTTPException(status_code=404, detail="Result not found")
998
+ # return Response(
999
+ # content=img_bytes,
1000
+ # media_type="image/png",
1001
+ # headers={"Content-Disposition": "inline; filename=result.png"}
1002
+ # )
1003
+
1004
+ # # --------------------- Mount Gradio ---------------------
1005
+ # fastapi_app = mount_gradio_app(fastapi_app, demo, path="/gradio")
1006
+
1007
+ # if __name__ == "__main__":
1008
+ # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)