LogicGoInfotechSpaces commited on
Commit
134e37f
·
verified ·
1 Parent(s): 09925e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2009 -40
app.py CHANGED
@@ -222,8 +222,6 @@ def init_codeformer_in_process():
222
  sys.path.insert(0, codeformer_root)
223
 
224
  from basicsr.utils.registry import ARCH_REGISTRY
225
- from basicsr.archs.rrdbnet_arch import RRDBNet
226
- from basicsr.utils.realesrgan_utils import RealESRGANer
227
  from basicsr.utils.download_util import load_file_from_url
228
  from facelib.utils.face_restoration_helper import FaceRestoreHelper
229
 
@@ -245,28 +243,17 @@ def init_codeformer_in_process():
245
  net.eval()
246
  codeformer_net = net
247
 
248
- # 2) Load RealESRGAN upsampler
249
- use_half = False
250
- if torch.cuda.is_available():
251
- no_half_gpu_list = ['1650', '1660']
252
- if not any(gpu in torch.cuda.get_device_name(0) for gpu in no_half_gpu_list):
253
- use_half = True
254
-
255
- model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=2)
256
- codeformer_upsampler = RealESRGANer(
257
- scale=2,
258
- model_path="https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/RealESRGAN_x2plus.pth",
259
- model=model,
260
- tile=400,
261
- tile_pad=40,
262
- pre_pad=0,
263
- half=use_half
264
- )
265
 
266
  # 3) Create FaceRestoreHelper (reused per request)
267
  # NOTE: local CodeFormer uses "upscale_factor" (not "upscale")
 
268
  codeformer_face_helper = FaceRestoreHelper(
269
- upscale_factor=2,
270
  face_size=512,
271
  crop_ratio=(1, 1),
272
  det_model='retinaface_resnet50',
@@ -393,7 +380,8 @@ def enhance_image_with_codeformer(rgb_img, temp_dir=None, w=0.7):
393
 
394
  codeformer_face_helper.align_warp_face()
395
 
396
- # Enhance each cropped face
 
397
  for idx, cropped_face in enumerate(codeformer_face_helper.cropped_faces):
398
  cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
399
  torch_normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
@@ -411,25 +399,17 @@ def enhance_image_with_codeformer(rgb_img, temp_dir=None, w=0.7):
411
 
412
  restored_face = restored_face.astype('uint8')
413
  codeformer_face_helper.add_restored_face(restored_face, cropped_face)
 
414
 
415
- # Paste back
416
- bg_img = None
417
- if codeformer_upsampler is not None:
418
- try:
419
- bg_img = codeformer_upsampler.enhance(bgr_img, outscale=2)[0]
420
- except Exception as e:
421
- logger.warning(f"[CodeFormer] Background upsampling failed: {e}")
422
-
423
  codeformer_face_helper.get_inverse_affine(None)
424
-
425
- if codeformer_upsampler is not None:
426
- restored_img = codeformer_face_helper.paste_faces_to_input_image(
427
- upsample_img=bg_img, draw_box=False, face_upsampler=codeformer_upsampler
428
- )
429
- else:
430
- restored_img = codeformer_face_helper.paste_faces_to_input_image(
431
- upsample_img=bg_img, draw_box=False
432
- )
433
 
434
  logger.info(f"[CodeFormer] In-process enhancement done in {time.time()-t0:.2f}s")
435
  return cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB)
@@ -449,8 +429,8 @@ def enhance_image_with_codeformer(rgb_img, temp_dir=None, w=0.7):
449
  f"-w {w} "
450
  f"--input_path {input_path} "
451
  f"--output_path {temp_dir} "
452
- f"--bg_upsampler realesrgan "
453
- f"--face_upsample"
454
  )
455
 
456
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
@@ -1987,6 +1967,1995 @@ if __name__ == "__main__":
1987
 
1988
 
1989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1990
 
1991
 
1992
 
 
222
  sys.path.insert(0, codeformer_root)
223
 
224
  from basicsr.utils.registry import ARCH_REGISTRY
 
 
225
  from basicsr.utils.download_util import load_file_from_url
226
  from facelib.utils.face_restoration_helper import FaceRestoreHelper
227
 
 
243
  net.eval()
244
  codeformer_net = net
245
 
246
+ # 2) RealESRGAN upsampler — SKIPPED for face swap
247
+ # Background/face upsampling is the #1 bottleneck (~20s per image).
248
+ # For face swap we only need CodeFormer face restoration, not super-resolution.
249
+ # The upsampler is kept as None; we no longer download the 64MB model at startup.
250
+ codeformer_upsampler = None
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
  # 3) Create FaceRestoreHelper (reused per request)
253
  # NOTE: local CodeFormer uses "upscale_factor" (not "upscale")
254
+ # upscale_factor=1 → keep original resolution (no 2x upscale needed for face swap)
255
  codeformer_face_helper = FaceRestoreHelper(
256
+ upscale_factor=1,
257
  face_size=512,
258
  crop_ratio=(1, 1),
259
  det_model='retinaface_resnet50',
 
380
 
381
  codeformer_face_helper.align_warp_face()
382
 
383
+ # Enhance each cropped face with CodeFormer neural net
384
+ t_faces = time.time()
385
  for idx, cropped_face in enumerate(codeformer_face_helper.cropped_faces):
386
  cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
387
  torch_normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
 
399
 
400
  restored_face = restored_face.astype('uint8')
401
  codeformer_face_helper.add_restored_face(restored_face, cropped_face)
402
+ logger.info(f"[CodeFormer] Face restoration ({num_faces} faces): {time.time()-t_faces:.2f}s")
403
 
404
+ # Paste restored faces back onto original image
405
+ # NOTE: We skip RealESRGAN background/face upsampling — it's the #1 bottleneck
406
+ # (~20s) and unnecessary for face swap. We only need CodeFormer face restoration.
407
+ t_paste = time.time()
 
 
 
 
408
  codeformer_face_helper.get_inverse_affine(None)
409
+ restored_img = codeformer_face_helper.paste_faces_to_input_image(
410
+ upsample_img=None, draw_box=False
411
+ )
412
+ logger.info(f"[CodeFormer] Paste back: {time.time()-t_paste:.2f}s")
 
 
 
 
 
413
 
414
  logger.info(f"[CodeFormer] In-process enhancement done in {time.time()-t0:.2f}s")
415
  return cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB)
 
429
  f"-w {w} "
430
  f"--input_path {input_path} "
431
  f"--output_path {temp_dir} "
432
+ f"--bg_upsampler None "
433
+ f"--upscale 1"
434
  )
435
 
436
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
 
1967
 
1968
 
1969
 
1970
+ # #####################working codee___________________##############
1971
+ # import os
1972
+ # os.environ["OMP_NUM_THREADS"] = "1"
1973
+ # import shutil
1974
+ # import uuid
1975
+ # import cv2
1976
+ # import numpy as np
1977
+ # import threading
1978
+ # import asyncio
1979
+ # import subprocess
1980
+ # import logging
1981
+ # import tempfile
1982
+ # import sys
1983
+ # import time
1984
+ # from datetime import datetime,timedelta
1985
+ # import tempfile
1986
+ # import insightface
1987
+ # from insightface.app import FaceAnalysis
1988
+ # from huggingface_hub import hf_hub_download
1989
+ # from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
1990
+ # from fastapi.responses import RedirectResponse
1991
+ # from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
1992
+ # from motor.motor_asyncio import AsyncIOMotorClient
1993
+ # from bson import ObjectId
1994
+ # from bson.errors import InvalidId
1995
+ # import httpx
1996
+ # import uvicorn
1997
+ # from PIL import Image
1998
+ # import io
1999
+ # import requests
2000
+ # # DigitalOcean Spaces
2001
+ # import boto3
2002
+ # from botocore.client import Config
2003
+ # from typing import Optional
2004
+
2005
+ # # --------------------- Logging ---------------------
2006
+ # logging.basicConfig(level=logging.INFO)
2007
+ # logger = logging.getLogger(__name__)
2008
+
2009
+ # # --------------------- Secrets & Paths ---------------------
2010
+ # REPO_ID = "HariLogicgo/face_swap_models"
2011
+ # MODELS_DIR = "./models"
2012
+ # os.makedirs(MODELS_DIR, exist_ok=True)
2013
+
2014
+ # HF_TOKEN = os.getenv("HF_TOKEN")
2015
+ # API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN")
2016
+
2017
+ # DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
2018
+ # DO_SPACES_ENDPOINT = f"https://{DO_SPACES_REGION}.digitaloceanspaces.com"
2019
+ # DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
2020
+ # DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
2021
+ # DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
2022
+
2023
+ # # NEW admin DB (with error handling for missing env vars)
2024
+ # ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
2025
+ # admin_client = None
2026
+ # admin_db = None
2027
+ # subcategories_col = None
2028
+ # media_clicks_col = None
2029
+ # if ADMIN_MONGO_URL:
2030
+ # try:
2031
+ # admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
2032
+ # admin_db = admin_client.adminPanel
2033
+ # subcategories_col = admin_db.subcategories
2034
+ # media_clicks_col = admin_db.media_clicks
2035
+ # except Exception as e:
2036
+ # logger.warning(f"MongoDB admin connection failed (optional): {e}")
2037
+
2038
+ # # Collage Maker DB (optional)
2039
+ # COLLAGE_MAKER_DB_URL = os.getenv("COLLAGE_MAKER_DB_URL")
2040
+ # collage_maker_client = None
2041
+ # collage_maker_db = None
2042
+ # collage_media_clicks_col = None
2043
+ # collage_subcategories_col = None
2044
+ # if COLLAGE_MAKER_DB_URL:
2045
+ # try:
2046
+ # collage_maker_client = AsyncIOMotorClient(COLLAGE_MAKER_DB_URL)
2047
+ # collage_maker_db = collage_maker_client.adminPanel
2048
+ # collage_media_clicks_col = collage_maker_db.media_clicks
2049
+ # collage_subcategories_col = collage_maker_db.subcategories
2050
+ # except Exception as e:
2051
+ # logger.warning(f"MongoDB collage-maker connection failed (optional): {e}")
2052
+
2053
+ # # AI Enhancer DB (optional)
2054
+
2055
+ # AI_ENHANCER_DB_URL = os.getenv("AI_ENHANCER_DB_URL")
2056
+ # ai_enhancer_client = None
2057
+ # ai_enhancer_db = None
2058
+ # ai_enhancer_media_clicks_col = None
2059
+ # ai_enhancer_subcategories_col = None
2060
+
2061
+ # if AI_ENHANCER_DB_URL:
2062
+ # try:
2063
+ # ai_enhancer_client = AsyncIOMotorClient(AI_ENHANCER_DB_URL)
2064
+ # ai_enhancer_db = ai_enhancer_client.test # 🔴 test database
2065
+ # ai_enhancer_media_clicks_col = ai_enhancer_db.media_clicks
2066
+ # ai_enhancer_subcategories_col = ai_enhancer_db.subcategories
2067
+ # except Exception as e:
2068
+ # logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
2069
+
2070
+
2071
+ # def get_media_clicks_collection(appname: Optional[str] = None):
2072
+ # """Return the media clicks collection for the given app (default: main admin)."""
2073
+ # if appname and str(appname).strip().lower() == "collage-maker":
2074
+ # return collage_media_clicks_col
2075
+ # return media_clicks_col
2076
+
2077
+
2078
+ # # OLD logs DB
2079
+ # MONGODB_URL = os.getenv("MONGODB_URL")
2080
+ # client = None
2081
+ # database = None
2082
+
2083
+ # # --------------------- Download Models ---------------------
2084
+ # def download_models():
2085
+ # try:
2086
+ # logger.info("Downloading models...")
2087
+ # inswapper_path = hf_hub_download(
2088
+ # repo_id=REPO_ID,
2089
+ # filename="models/inswapper_128.onnx",
2090
+ # repo_type="model",
2091
+ # local_dir=MODELS_DIR,
2092
+ # token=HF_TOKEN
2093
+ # )
2094
+
2095
+ # buffalo_files = ["1k3d68.onnx", "2d106det.onnx", "genderage.onnx", "det_10g.onnx", "w600k_r50.onnx"]
2096
+ # for f in buffalo_files:
2097
+ # hf_hub_download(
2098
+ # repo_id=REPO_ID,
2099
+ # filename=f"models/buffalo_l/" + f,
2100
+ # repo_type="model",
2101
+ # local_dir=MODELS_DIR,
2102
+ # token=HF_TOKEN
2103
+ # )
2104
+
2105
+ # logger.info("Models downloaded successfully.")
2106
+ # return inswapper_path
2107
+ # except Exception as e:
2108
+ # logger.error(f"Model download failed: {e}")
2109
+ # raise
2110
+
2111
+ # try:
2112
+ # inswapper_path = download_models()
2113
+
2114
+ # # --------------------- Face Analysis + Swapper ---------------------
2115
+ # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
2116
+ # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
2117
+ # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
2118
+ # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
2119
+ # logger.info("Face analysis models loaded successfully")
2120
+ # except Exception as e:
2121
+ # logger.error(f"Failed to initialize face analysis models: {e}")
2122
+ # # Set defaults to prevent crash
2123
+ # inswapper_path = None
2124
+ # face_analysis_app = None
2125
+ # swapper = None
2126
+
2127
+ # # --------------------- CodeFormer ---------------------
2128
+ # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
2129
+
2130
+ # def ensure_codeformer():
2131
+ # """
2132
+ # Ensure CodeFormer's local basicsr + facelib are importable and
2133
+ # pretrained weights are downloaded. No setup.py needed — we use
2134
+ # sys.path / PYTHONPATH instead.
2135
+ # """
2136
+ # try:
2137
+ # if not os.path.exists("CodeFormer"):
2138
+ # logger.info("CodeFormer not found, cloning repository...")
2139
+ # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
2140
+ # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=False)
2141
+
2142
+ # # Add CodeFormer root to sys.path so `import basicsr` and
2143
+ # # `import facelib` resolve to the local (compatible) versions
2144
+ # # instead of the broken PyPI basicsr==1.4.2.
2145
+ # codeformer_root = os.path.join(os.getcwd(), "CodeFormer")
2146
+ # if codeformer_root not in sys.path:
2147
+ # sys.path.insert(0, codeformer_root)
2148
+ # logger.info(f"Added {codeformer_root} to sys.path for local basicsr/facelib")
2149
+
2150
+ # # NOTE: We do NOT need the PyPI 'realesrgan' package.
2151
+ # # Both in-process and subprocess paths use CodeFormer's local
2152
+ # # basicsr.utils.realesrgan_utils.RealESRGANer instead.
2153
+ # # Installing PyPI realesrgan at runtime would re-install the
2154
+ # # broken basicsr==1.4.2 and break everything.
2155
+
2156
+ # # Download pretrained weights if not already present
2157
+ # if os.path.exists("CodeFormer"):
2158
+ # try:
2159
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=False, timeout=300)
2160
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
2161
+ # logger.warning("Failed to download facelib models (optional)")
2162
+ # try:
2163
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=False, timeout=300)
2164
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
2165
+ # logger.warning("Failed to download CodeFormer models (optional)")
2166
+ # except Exception as e:
2167
+ # logger.error(f"CodeFormer setup failed: {e}")
2168
+ # logger.warning("Continuing without CodeFormer features...")
2169
+
2170
+ # ensure_codeformer()
2171
+
2172
+ # # --------------------- In-Process CodeFormer (No Subprocess!) ---------------------
2173
+ # # Load CodeFormer models ONCE at startup instead of spawning a new Python process per request.
2174
+ # # This eliminates 15-40s of model loading overhead per request.
2175
+
2176
+ # codeformer_net = None
2177
+ # codeformer_upsampler = None
2178
+ # codeformer_face_helper = None
2179
+ # codeformer_device = None
2180
+
2181
+ # def init_codeformer_in_process():
2182
+ # """Load CodeFormer models once into memory for fast per-request inference."""
2183
+ # global codeformer_net, codeformer_upsampler, codeformer_face_helper, codeformer_device
2184
+ # try:
2185
+ # import torch
2186
+ # from torchvision.transforms.functional import normalize as torch_normalize
2187
+
2188
+ # # Add CodeFormer to Python path
2189
+ # codeformer_root = os.path.join(os.getcwd(), "CodeFormer")
2190
+ # if codeformer_root not in sys.path:
2191
+ # sys.path.insert(0, codeformer_root)
2192
+
2193
+ # from basicsr.utils.registry import ARCH_REGISTRY
2194
+ # from basicsr.archs.rrdbnet_arch import RRDBNet
2195
+ # from basicsr.utils.realesrgan_utils import RealESRGANer
2196
+ # from basicsr.utils.download_util import load_file_from_url
2197
+ # from facelib.utils.face_restoration_helper import FaceRestoreHelper
2198
+
2199
+ # codeformer_device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
2200
+ # logger.info(f"Initializing CodeFormer on device: {codeformer_device}")
2201
+
2202
+ # # 1) Load CodeFormer network
2203
+ # net = ARCH_REGISTRY.get('CodeFormer')(
2204
+ # dim_embd=512, codebook_size=1024, n_head=8, n_layers=9,
2205
+ # connect_list=['32', '64', '128', '256']
2206
+ # ).to(codeformer_device)
2207
+
2208
+ # ckpt_path = load_file_from_url(
2209
+ # url='https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth',
2210
+ # model_dir='weights/CodeFormer', progress=True, file_name=None
2211
+ # )
2212
+ # checkpoint = torch.load(ckpt_path, map_location=codeformer_device)['params_ema']
2213
+ # net.load_state_dict(checkpoint)
2214
+ # net.eval()
2215
+ # codeformer_net = net
2216
+
2217
+ # # 2) Load RealESRGAN upsampler
2218
+ # use_half = False
2219
+ # if torch.cuda.is_available():
2220
+ # no_half_gpu_list = ['1650', '1660']
2221
+ # if not any(gpu in torch.cuda.get_device_name(0) for gpu in no_half_gpu_list):
2222
+ # use_half = True
2223
+
2224
+ # model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=2)
2225
+ # codeformer_upsampler = RealESRGANer(
2226
+ # scale=2,
2227
+ # model_path="https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/RealESRGAN_x2plus.pth",
2228
+ # model=model,
2229
+ # tile=400,
2230
+ # tile_pad=40,
2231
+ # pre_pad=0,
2232
+ # half=use_half
2233
+ # )
2234
+
2235
+ # # 3) Create FaceRestoreHelper (reused per request)
2236
+ # # NOTE: local CodeFormer uses "upscale_factor" (not "upscale")
2237
+ # codeformer_face_helper = FaceRestoreHelper(
2238
+ # upscale_factor=2,
2239
+ # face_size=512,
2240
+ # crop_ratio=(1, 1),
2241
+ # det_model='retinaface_resnet50',
2242
+ # save_ext='png',
2243
+ # use_parse=True,
2244
+ # device=codeformer_device
2245
+ # )
2246
+
2247
+ # logger.info("✅ CodeFormer models loaded in-process successfully!")
2248
+ # return True
2249
+ # except Exception as e:
2250
+ # logger.error(f"Failed to load CodeFormer in-process: {e}")
2251
+ # logger.warning("CodeFormer enhancement will be unavailable.")
2252
+ # return False
2253
+
2254
+ # # Try to load CodeFormer models in-process
2255
+ # _codeformer_available = init_codeformer_in_process()
2256
+ # # --------------------- FastAPI ---------------------
2257
+ # fastapi_app = FastAPI()
2258
+
2259
+ # @fastapi_app.on_event("startup")
2260
+ # async def startup_db():
2261
+ # global client, database
2262
+ # if MONGODB_URL:
2263
+ # try:
2264
+ # logger.info("Initializing MongoDB for API logs...")
2265
+ # client = AsyncIOMotorClient(MONGODB_URL)
2266
+ # database = client.FaceSwap
2267
+ # logger.info("MongoDB initialized for API logs")
2268
+ # except Exception as e:
2269
+ # logger.warning(f"MongoDB connection failed (optional): {e}")
2270
+ # client = None
2271
+ # database = None
2272
+ # else:
2273
+ # logger.warning("MONGODB_URL not set, skipping MongoDB initialization")
2274
+
2275
+ # @fastapi_app.on_event("shutdown")
2276
+ # async def shutdown_db():
2277
+ # global client, admin_client, collage_maker_client
2278
+ # if client is not None:
2279
+ # client.close()
2280
+ # logger.info("MongoDB connection closed")
2281
+ # if admin_client is not None:
2282
+ # admin_client.close()
2283
+ # logger.info("Admin MongoDB connection closed")
2284
+ # if collage_maker_client is not None:
2285
+ # collage_maker_client.close()
2286
+ # logger.info("Collage Maker MongoDB connection closed")
2287
+
2288
+ # # --------------------- Auth ---------------------
2289
+ # security = HTTPBearer()
2290
+
2291
+ # def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
2292
+ # if credentials.credentials != API_SECRET_TOKEN:
2293
+ # raise HTTPException(status_code=401, detail="Invalid or missing token")
2294
+ # return credentials.credentials
2295
+
2296
+ # # --------------------- DB Selector ---------------------
2297
+ # def get_app_db_collections(appname: Optional[str] = None):
2298
+ # """
2299
+ # Returns (media_clicks_collection, subcategories_collection)
2300
+ # based on appname.
2301
+ # """
2302
+
2303
+ # if appname:
2304
+ # app = appname.strip().lower()
2305
+
2306
+ # if app == "collage-maker":
2307
+ # if collage_media_clicks_col is not None and collage_subcategories_col is not None:
2308
+ # return collage_media_clicks_col, collage_subcategories_col
2309
+ # logger.warning("Collage-maker DB not configured, falling back to admin")
2310
+
2311
+ # elif app == "ai-enhancer":
2312
+ # if ai_enhancer_media_clicks_col is not None and ai_enhancer_subcategories_col is not None:
2313
+ # return ai_enhancer_media_clicks_col, ai_enhancer_subcategories_col
2314
+ # logger.warning("AI-Enhancer DB not configured, falling back to admin")
2315
+
2316
+ # # default fallback
2317
+ # return media_clicks_col, subcategories_col
2318
+
2319
+
2320
+
2321
+ # # --------------------- Logging API Hits ---------------------
2322
+ # async def log_faceswap_hit(token: str, status: str = "success"):
2323
+ # global database
2324
+ # if database is None:
2325
+ # return
2326
+ # await database.api_logs.insert_one({
2327
+ # "token": token,
2328
+ # "endpoint": "/faceswap",
2329
+ # "status": status,
2330
+ # "timestamp": datetime.utcnow()
2331
+ # })
2332
+
2333
+ # # --------------------- Face Swap Pipeline ---------------------
2334
+ # swap_lock = threading.Lock()
2335
+
2336
+ # def enhance_image_with_codeformer(rgb_img, temp_dir=None, w=0.7):
2337
+ # """
2338
+ # Enhance face image using CodeFormer.
2339
+ # Uses in-process models (fast) if available, falls back to subprocess (slow).
2340
+ # """
2341
+ # global codeformer_net, codeformer_upsampler, codeformer_face_helper, codeformer_device
2342
+
2343
+ # t0 = time.time()
2344
+
2345
+ # # ── FAST PATH: In-process CodeFormer (no subprocess!) ──
2346
+ # if codeformer_net is not None and codeformer_face_helper is not None:
2347
+ # import torch
2348
+ # from torchvision.transforms.functional import normalize as torch_normalize
2349
+ # from basicsr.utils import img2tensor, tensor2img
2350
+ # from facelib.utils.misc import is_gray
2351
+
2352
+ # bgr_img = cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR)
2353
+
2354
+ # # Reset face helper state
2355
+ # codeformer_face_helper.clean_all()
2356
+ # codeformer_face_helper.read_image(bgr_img)
2357
+
2358
+ # num_faces = codeformer_face_helper.get_face_landmarks_5(
2359
+ # only_center_face=False, resize=640, eye_dist_threshold=5
2360
+ # )
2361
+ # logger.info(f"[CodeFormer] Detected {num_faces} faces in {time.time()-t0:.2f}s")
2362
+
2363
+ # codeformer_face_helper.align_warp_face()
2364
+
2365
+ # # Enhance each cropped face
2366
+ # for idx, cropped_face in enumerate(codeformer_face_helper.cropped_faces):
2367
+ # cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
2368
+ # torch_normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
2369
+ # cropped_face_t = cropped_face_t.unsqueeze(0).to(codeformer_device)
2370
+
2371
+ # try:
2372
+ # with torch.no_grad():
2373
+ # output = codeformer_net(cropped_face_t, w=w, adain=True)[0]
2374
+ # restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
2375
+ # del output
2376
+ # torch.cuda.empty_cache()
2377
+ # except Exception as e:
2378
+ # logger.warning(f"[CodeFormer] Face {idx} inference failed: {e}")
2379
+ # restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
2380
+
2381
+ # restored_face = restored_face.astype('uint8')
2382
+ # codeformer_face_helper.add_restored_face(restored_face, cropped_face)
2383
+
2384
+ # # Paste back
2385
+ # bg_img = None
2386
+ # if codeformer_upsampler is not None:
2387
+ # try:
2388
+ # bg_img = codeformer_upsampler.enhance(bgr_img, outscale=2)[0]
2389
+ # except Exception as e:
2390
+ # logger.warning(f"[CodeFormer] Background upsampling failed: {e}")
2391
+
2392
+ # codeformer_face_helper.get_inverse_affine(None)
2393
+
2394
+ # if codeformer_upsampler is not None:
2395
+ # restored_img = codeformer_face_helper.paste_faces_to_input_image(
2396
+ # upsample_img=bg_img, draw_box=False, face_upsampler=codeformer_upsampler
2397
+ # )
2398
+ # else:
2399
+ # restored_img = codeformer_face_helper.paste_faces_to_input_image(
2400
+ # upsample_img=bg_img, draw_box=False
2401
+ # )
2402
+
2403
+ # logger.info(f"[CodeFormer] In-process enhancement done in {time.time()-t0:.2f}s")
2404
+ # return cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB)
2405
+
2406
+ # # ── SLOW FALLBACK: Subprocess CodeFormer (with timeout!) ──
2407
+ # logger.warning("[CodeFormer] In-process models unavailable, falling back to subprocess")
2408
+ # if temp_dir is None:
2409
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"enhance_{uuid.uuid4().hex[:8]}")
2410
+ # os.makedirs(temp_dir, exist_ok=True)
2411
+
2412
+ # input_path = os.path.join(temp_dir, "input.jpg")
2413
+ # cv2.imwrite(input_path, cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR))
2414
+
2415
+ # python_cmd = sys.executable if sys.executable else "python3"
2416
+ # cmd = (
2417
+ # f"{python_cmd} {CODEFORMER_PATH} "
2418
+ # f"-w {w} "
2419
+ # f"--input_path {input_path} "
2420
+ # f"--output_path {temp_dir} "
2421
+ # f"--bg_upsampler realesrgan "
2422
+ # f"--face_upsample"
2423
+ # )
2424
+
2425
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
2426
+ # if result.returncode != 0:
2427
+ # raise RuntimeError(result.stderr)
2428
+
2429
+ # final_dir = os.path.join(temp_dir, "final_results")
2430
+ # files = [f for f in os.listdir(final_dir) if f.endswith(".png")]
2431
+ # if not files:
2432
+ # raise RuntimeError("No enhanced output")
2433
+
2434
+ # final_path = os.path.join(final_dir, files[0])
2435
+ # enhanced = cv2.imread(final_path)
2436
+ # logger.info(f"[CodeFormer] Subprocess enhancement done in {time.time()-t0:.2f}s")
2437
+ # return cv2.cvtColor(enhanced, cv2.COLOR_BGR2RGB)
2438
+
2439
+ # def multi_face_swap(src_img, tgt_img):
2440
+ # pipeline_start = time.time()
2441
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
2442
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
2443
+
2444
+ # t0 = time.time()
2445
+ # src_faces = face_analysis_app.get(src_bgr)
2446
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
2447
+ # logger.info(f"[Pipeline] Multi-face detection: {time.time()-t0:.2f}s")
2448
+
2449
+ # if not src_faces or not tgt_faces:
2450
+ # raise ValueError("No faces detected")
2451
+
2452
+ # def face_sort_key(face):
2453
+ # x1, y1, x2, y2 = face.bbox
2454
+ # area = (x2 - x1) * (y2 - y1)
2455
+ # cx = (x1 + x2) / 2
2456
+ # return (-area, cx)
2457
+
2458
+ # src_male = sorted([f for f in src_faces if f.gender == 1], key=face_sort_key)
2459
+ # src_female = sorted([f for f in src_faces if f.gender == 0], key=face_sort_key)
2460
+ # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
2461
+ # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
2462
+
2463
+ # pairs = []
2464
+ # for s, t in zip(src_male, tgt_male):
2465
+ # pairs.append((s, t))
2466
+ # for s, t in zip(src_female, tgt_female):
2467
+ # pairs.append((s, t))
2468
+
2469
+ # if not pairs:
2470
+ # src_faces = sorted(src_faces, key=face_sort_key)
2471
+ # tgt_faces = sorted(tgt_faces, key=face_sort_key)
2472
+ # pairs = list(zip(src_faces, tgt_faces))
2473
+
2474
+ # t0 = time.time()
2475
+ # result_img = tgt_bgr.copy()
2476
+ # for src_face, _ in pairs:
2477
+ # if face_analysis_app is None:
2478
+ # raise ValueError("Face analysis models not initialized.")
2479
+ # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
2480
+ # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
2481
+ # target_face = candidates[0]
2482
+
2483
+ # if swapper is None:
2484
+ # raise ValueError("Face swap models not initialized.")
2485
+ # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
2486
+ # logger.info(f"[Pipeline] Multi-face swap ({len(pairs)} pairs): {time.time()-t0:.2f}s")
2487
+
2488
+ # logger.info(f"[Pipeline] TOTAL multi_face_swap: {time.time()-pipeline_start:.2f}s")
2489
+ # return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
2490
+
2491
+
2492
+
2493
+ # def face_swap_and_enhance(src_img, tgt_img, temp_dir=None):
2494
+ # try:
2495
+ # with swap_lock:
2496
+ # pipeline_start = time.time()
2497
+
2498
+ # if temp_dir is None:
2499
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_work_{uuid.uuid4().hex[:8]}")
2500
+ # if os.path.exists(temp_dir):
2501
+ # shutil.rmtree(temp_dir)
2502
+ # os.makedirs(temp_dir, exist_ok=True)
2503
+
2504
+ # if face_analysis_app is None:
2505
+ # return None, None, "❌ Face analysis models not initialized."
2506
+ # if swapper is None:
2507
+ # return None, None, "❌ Face swap models not initialized."
2508
+
2509
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
2510
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
2511
+
2512
+ # t0 = time.time()
2513
+ # src_faces = face_analysis_app.get(src_bgr)
2514
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
2515
+ # logger.info(f"[Pipeline] Face detection: {time.time()-t0:.2f}s")
2516
+
2517
+ # if not src_faces or not tgt_faces:
2518
+ # return None, None, "❌ Face not detected in one of the images"
2519
+
2520
+ # t0 = time.time()
2521
+ # swapped_bgr = swapper.get(tgt_bgr, tgt_faces[0], src_faces[0])
2522
+ # logger.info(f"[Pipeline] Face swap: {time.time()-t0:.2f}s")
2523
+
2524
+ # if swapped_bgr is None:
2525
+ # return None, None, "❌ Face swap failed"
2526
+
2527
+ # # Use in-process CodeFormer enhancement (fast path)
2528
+ # t0 = time.time()
2529
+ # swapped_rgb = cv2.cvtColor(swapped_bgr, cv2.COLOR_BGR2RGB)
2530
+ # try:
2531
+ # enhanced_rgb = enhance_image_with_codeformer(swapped_rgb)
2532
+ # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
2533
+ # except Exception as e:
2534
+ # logger.error(f"[Pipeline] CodeFormer failed, using raw swap: {e}")
2535
+ # enhanced_bgr = swapped_bgr
2536
+ # logger.info(f"[Pipeline] Enhancement: {time.time()-t0:.2f}s")
2537
+
2538
+ # final_path = os.path.join(temp_dir, f"result_{uuid.uuid4().hex[:8]}.png")
2539
+ # cv2.imwrite(final_path, enhanced_bgr)
2540
+
2541
+ # final_img = cv2.cvtColor(enhanced_bgr, cv2.COLOR_BGR2RGB)
2542
+
2543
+ # logger.info(f"[Pipeline] TOTAL face_swap_and_enhance: {time.time()-pipeline_start:.2f}s")
2544
+ # return final_img, final_path, ""
2545
+
2546
+ # except Exception as e:
2547
+ # return None, None, f"❌ Error: {str(e)}"
2548
+
2549
+ # def compress_image(
2550
+ # image_bytes: bytes,
2551
+ # max_size=(1280, 1280), # max width/height
2552
+ # quality=75 # JPEG quality (60–80 is ideal)
2553
+ # ) -> bytes:
2554
+ # """
2555
+ # Compress image by resizing and lowering quality.
2556
+ # Returns compressed image bytes.
2557
+ # """
2558
+ # img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
2559
+
2560
+ # # Resize while maintaining aspect ratio
2561
+ # img.thumbnail(max_size, Image.LANCZOS)
2562
+
2563
+ # output = io.BytesIO()
2564
+ # img.save(
2565
+ # output,
2566
+ # format="JPEG",
2567
+ # quality=quality,
2568
+ # optimize=True,
2569
+ # progressive=True
2570
+ # )
2571
+
2572
+ # return output.getvalue()
2573
+
2574
+ # # --------------------- DigitalOcean Spaces Helper ---------------------
2575
+ # def get_spaces_client():
2576
+ # session = boto3.session.Session()
2577
+ # client = session.client(
2578
+ # 's3',
2579
+ # region_name=DO_SPACES_REGION,
2580
+ # endpoint_url=DO_SPACES_ENDPOINT,
2581
+ # aws_access_key_id=DO_SPACES_KEY,
2582
+ # aws_secret_access_key=DO_SPACES_SECRET,
2583
+ # config=Config(signature_version='s3v4')
2584
+ # )
2585
+ # return client
2586
+
2587
+ # def upload_to_spaces(file_bytes, key, content_type="image/png"):
2588
+ # client = get_spaces_client()
2589
+ # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
2590
+ # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
2591
+
2592
+ # def download_from_spaces(key):
2593
+ # client = get_spaces_client()
2594
+ # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
2595
+ # return obj['Body'].read()
2596
+
2597
+ # def mandatory_enhancement(rgb_img):
2598
+ # """
2599
+ # Always runs CodeFormer on the final image.
2600
+ # Fail-safe: returns original if enhancement fails.
2601
+ # """
2602
+ # try:
2603
+ # return enhance_image_with_codeformer(rgb_img)
2604
+ # except Exception as e:
2605
+ # logger.error(f"CodeFormer failed, returning original: {e}")
2606
+ # return rgb_img
2607
+
2608
+ # # --------------------- API Endpoints ---------------------
2609
+ # @fastapi_app.get("/")
2610
+ # async def root():
2611
+ # """Root endpoint"""
2612
+ # return {
2613
+ # "success": True,
2614
+ # "message": "FaceSwap API",
2615
+ # "data": {
2616
+ # "version": "1.0.0",
2617
+ # "Product Name":"Beauty Camera - GlowCam AI Studio",
2618
+ # "Released By" : "LogicGo Infotech"
2619
+ # }
2620
+ # }
2621
+ # @fastapi_app.get("/health")
2622
+ # async def health():
2623
+ # return {"status": "healthy"}
2624
+
2625
+ # @fastapi_app.get("/test-admin-db")
2626
+ # async def test_admin_db():
2627
+ # try:
2628
+ # doc = await admin_db.list_collection_names()
2629
+ # return {"ok": True, "collections": doc}
2630
+ # except Exception as e:
2631
+ # return {"ok": False, "error": str(e), "url": ADMIN_MONGO_URL}
2632
+
2633
+ # # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
2634
+ # # async def face_swap_api(
2635
+ # # source: UploadFile = File(...),
2636
+ # # target_category_id: str = Form(None),
2637
+ # # new_category_id: str = Form(None),
2638
+ # # user_id: Optional[str] = Form(None),
2639
+ # # appname: Optional[str] = Form(None),
2640
+ # # credentials: HTTPAuthorizationCredentials = Security(security)
2641
+ # # ):
2642
+ # # start_time = datetime.utcnow()
2643
+
2644
+ # # try:
2645
+ # # # ------------------------------------------------------------------
2646
+ # # # VALIDATION
2647
+ # # # ------------------------------------------------------------------
2648
+ # # # --------------------------------------------------------------
2649
+ # # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
2650
+ # # # --------------------------------------------------------------
2651
+ # # if target_category_id == "":
2652
+ # # target_category_id = None
2653
+
2654
+ # # if new_category_id == "":
2655
+ # # new_category_id = None
2656
+
2657
+ # # if user_id == "":
2658
+ # # user_id = None
2659
+
2660
+ # # # media_clicks_collection = get_media_clicks_collection(appname)
2661
+ # # media_clicks_collection, subcategories_collection = get_app_db_collections(appname)
2662
+
2663
+
2664
+ # # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
2665
+
2666
+ # # if target_category_id and new_category_id:
2667
+ # # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
2668
+
2669
+ # # if not target_category_id and not new_category_id:
2670
+ # # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
2671
+
2672
+ # # # ------------------------------------------------------------------
2673
+ # # # READ SOURCE IMAGE
2674
+ # # # ------------------------------------------------------------------
2675
+ # # src_bytes = await source.read()
2676
+ # # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
2677
+ # # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
2678
+
2679
+ # # # ------------------------------------------------------------------
2680
+ # # # CASE 1 : new_category_id → MongoDB lookup
2681
+ # # # ------------------------------------------------------------------
2682
+ # # if new_category_id:
2683
+
2684
+ # # # doc = await subcategories_col.find_one({
2685
+ # # # "asset_images._id": ObjectId(new_category_id)
2686
+ # # # })
2687
+ # # doc = await subcategories_collection.find_one({
2688
+ # # "asset_images._id": ObjectId(new_category_id)
2689
+ # # })
2690
+
2691
+
2692
+ # # if not doc:
2693
+ # # raise HTTPException(404, "Asset image not found in database")
2694
+
2695
+ # # # extract correct asset
2696
+ # # asset = next(
2697
+ # # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
2698
+ # # None
2699
+ # # )
2700
+
2701
+ # # if not asset:
2702
+ # # raise HTTPException(404, "Asset image URL not found")
2703
+
2704
+ # # # correct URL
2705
+ # # target_url = asset["url"]
2706
+
2707
+ # # # correct categoryId (ObjectId)
2708
+ # # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
2709
+ # # subcategory_oid = doc["_id"]
2710
+
2711
+ # # # ------------------------------------------------------------------#
2712
+ # # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
2713
+ # # # ------------------------------------------------------------------#
2714
+ # # if user_id and media_clicks_collection is not None:
2715
+ # # try:
2716
+ # # user_id_clean = user_id.strip()
2717
+ # # if not user_id_clean:
2718
+ # # raise ValueError("user_id cannot be empty")
2719
+ # # try:
2720
+ # # user_oid = ObjectId(user_id_clean)
2721
+ # # except (InvalidId, ValueError) as e:
2722
+ # # logger.error(f"Invalid user_id format: {user_id_clean}")
2723
+ # # raise ValueError(f"Invalid user_id format: {user_id_clean}")
2724
+
2725
+ # # now = datetime.utcnow()
2726
+
2727
+ # # # Normalize dates (UTC midnight)
2728
+ # # today_date = datetime(now.year, now.month, now.day)
2729
+
2730
+ # # # -------------------------------------------------
2731
+ # # # STEP 1: Ensure root document exists
2732
+ # # # -------------------------------------------------
2733
+ # # await media_clicks_collection.update_one(
2734
+ # # {"userId": user_oid},
2735
+ # # {
2736
+ # # "$setOnInsert": {
2737
+ # # "userId": user_oid,
2738
+ # # "createdAt": now,
2739
+ # # "ai_edit_complete": 0,
2740
+ # # "ai_edit_daily_count": []
2741
+ # # }
2742
+ # # },
2743
+ # # upsert=True
2744
+ # # )
2745
+ # # # -------------------------------------------------
2746
+ # # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
2747
+ # # # -------------------------------------------------
2748
+ # # doc = await media_clicks_collection.find_one(
2749
+ # # {"userId": user_oid},
2750
+ # # {"ai_edit_daily_count": 1}
2751
+ # # )
2752
+
2753
+ # # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
2754
+
2755
+ # # # Normalize today to UTC midnight
2756
+ # # today_date = datetime(now.year, now.month, now.day)
2757
+
2758
+ # # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
2759
+ # # daily_map = {}
2760
+ # # for entry in daily_entries:
2761
+ # # d = entry["date"]
2762
+ # # if isinstance(d, datetime):
2763
+ # # d = datetime(d.year, d.month, d.day)
2764
+ # # daily_map[d] = entry["count"] # overwrite = no duplicates
2765
+
2766
+ # # # Determine last recorded date
2767
+ # # last_date = max(daily_map.keys()) if daily_map else today_date
2768
+
2769
+ # # # Fill ALL missing days with count = 0
2770
+ # # next_day = last_date + timedelta(days=1)
2771
+ # # while next_day < today_date:
2772
+ # # daily_map.setdefault(next_day, 0)
2773
+ # # next_day += timedelta(days=1)
2774
+
2775
+ # # # Mark today as used (binary)
2776
+ # # daily_map[today_date] = 1
2777
+
2778
+ # # # Rebuild list: OLDEST → NEWEST
2779
+ # # final_daily_entries = [
2780
+ # # {"date": d, "count": daily_map[d]}
2781
+ # # for d in sorted(daily_map.keys())
2782
+ # # ]
2783
+
2784
+ # # # Keep only last 32 days
2785
+ # # final_daily_entries = final_daily_entries[-32:]
2786
+
2787
+ # # # Atomic replace
2788
+ # # await media_clicks_collection.update_one(
2789
+ # # {"userId": user_oid},
2790
+ # # {
2791
+ # # "$set": {
2792
+ # # "ai_edit_daily_count": final_daily_entries,
2793
+ # # "updatedAt": now
2794
+ # # }
2795
+ # # }
2796
+ # # )
2797
+
2798
+ # # # -------------------------------------------------
2799
+ # # # STEP 3: Try updating existing subCategory
2800
+ # # # -------------------------------------------------
2801
+ # # update_result = await media_clicks_collection.update_one(
2802
+ # # {
2803
+ # # "userId": user_oid,
2804
+ # # "subCategories.subCategoryId": subcategory_oid
2805
+ # # },
2806
+ # # {
2807
+ # # "$inc": {
2808
+ # # "subCategories.$.click_count": 1,
2809
+ # # "ai_edit_complete": 1
2810
+ # # },
2811
+ # # "$set": {
2812
+ # # "subCategories.$.lastClickedAt": now,
2813
+ # # "ai_edit_last_date": now,
2814
+ # # "updatedAt": now
2815
+ # # }
2816
+ # # }
2817
+ # # )
2818
+
2819
+ # # # -------------------------------------------------
2820
+ # # # STEP 4: Push subCategory if missing
2821
+ # # # -------------------------------------------------
2822
+ # # if update_result.matched_count == 0:
2823
+ # # await media_clicks_collection.update_one(
2824
+ # # {"userId": user_oid},
2825
+ # # {
2826
+ # # "$inc": {
2827
+ # # "ai_edit_complete": 1
2828
+ # # },
2829
+ # # "$set": {
2830
+ # # "ai_edit_last_date": now,
2831
+ # # "updatedAt": now
2832
+ # # },
2833
+ # # "$push": {
2834
+ # # "subCategories": {
2835
+ # # "subCategoryId": subcategory_oid,
2836
+ # # "click_count": 1,
2837
+ # # "lastClickedAt": now
2838
+ # # }
2839
+ # # }
2840
+ # # }
2841
+ # # )
2842
+
2843
+ # # # -------------------------------------------------
2844
+ # # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
2845
+ # # # -------------------------------------------------
2846
+ # # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
2847
+ # # if user_doc and "subCategories" in user_doc:
2848
+ # # subcategories = user_doc["subCategories"]
2849
+ # # # Sort by lastClickedAt in ascending order (oldest first)
2850
+ # # # Handle missing or None dates by using datetime.min
2851
+ # # subcategories_sorted = sorted(
2852
+ # # subcategories,
2853
+ # # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
2854
+ # # )
2855
+ # # # Update with sorted array
2856
+ # # await media_clicks_collection.update_one(
2857
+ # # {"userId": user_oid},
2858
+ # # {
2859
+ # # "$set": {
2860
+ # # "subCategories": subcategories_sorted,
2861
+ # # "updatedAt": now
2862
+ # # }
2863
+ # # }
2864
+ # # )
2865
+
2866
+ # # logger.info(
2867
+ # # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
2868
+ # # user_id,
2869
+ # # str(subcategory_oid)
2870
+ # # )
2871
+
2872
+ # # except Exception as media_err:
2873
+ # # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
2874
+ # # elif user_id and media_clicks_collection is None:
2875
+ # # logger.warning("Media clicks collection unavailable; skipping media click tracking")
2876
+
2877
+ # # # # ------------------------------------------------------------------
2878
+ # # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
2879
+ # # # # ------------------------------------------------------------------
2880
+ # # if target_category_id:
2881
+ # # client = get_spaces_client()
2882
+ # # base_prefix = "faceswap/target/"
2883
+ # # resp = client.list_objects_v2(
2884
+ # # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
2885
+ # # )
2886
+
2887
+ # # # Extract categories from the CommonPrefixes
2888
+ # # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
2889
+
2890
+ # # target_url = None
2891
+
2892
+ # # # --- FIX STARTS HERE ---
2893
+ # # for category in categories:
2894
+ # # original_prefix = f"faceswap/target/{category}/original/"
2895
+ # # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
2896
+
2897
+ # # # List objects in original/
2898
+ # # original_objects = client.list_objects_v2(
2899
+ # # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
2900
+ # # ).get("Contents", [])
2901
+
2902
+ # # # List objects in thumb/ (optional: for the old code's extra check)
2903
+ # # thumb_objects = client.list_objects_v2(
2904
+ # # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
2905
+ # # ).get("Contents", [])
2906
+
2907
+ # # # Extract only the filenames and filter for .png
2908
+ # # original_filenames = sorted([
2909
+ # # obj["Key"].split("/")[-1] for obj in original_objects
2910
+ # # if obj["Key"].split("/")[-1].endswith(".png")
2911
+ # # ])
2912
+ # # thumb_filenames = [
2913
+ # # obj["Key"].split("/")[-1] for obj in thumb_objects
2914
+ # # ]
2915
+
2916
+ # # # Replicate the old indexing logic based on sorted filenames
2917
+ # # for idx, filename in enumerate(original_filenames, start=1):
2918
+ # # cid = f"{category.lower()}image_{idx}"
2919
+
2920
+ # # # Optional: Replicate the thumb file check for 100% parity
2921
+ # # # if filename in thumb_filenames and cid == target_category_id:
2922
+ # # # Simpler check just on the ID, assuming thumb files are present
2923
+ # # if cid == target_category_id:
2924
+ # # # Construct the final target URL using the full prefix and the filename
2925
+ # # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
2926
+ # # break
2927
+
2928
+ # # if target_url:
2929
+ # # break
2930
+ # # # --- FIX ENDS HERE ---
2931
+
2932
+ # # if not target_url:
2933
+ # # raise HTTPException(404, "Target categoryId not found")
2934
+ # # # # ------------------------------------------------------------------
2935
+ # # # # DOWNLOAD TARGET IMAGE
2936
+ # # # # ------------------------------------------------------------------
2937
+ # # async with httpx.AsyncClient(timeout=30.0) as client:
2938
+ # # response = await client.get(target_url)
2939
+ # # response.raise_for_status()
2940
+ # # tgt_bytes = response.content
2941
+
2942
+ # # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
2943
+ # # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
2944
+
2945
+ # # if src_bgr is None or tgt_bgr is None:
2946
+ # # raise HTTPException(400, "Invalid image data")
2947
+
2948
+ # # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
2949
+ # # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
2950
+
2951
+ # # # ------------------------------------------------------------------
2952
+ # # # FACE SWAP EXECUTION
2953
+ # # # ------------------------------------------------------------------
2954
+ # # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
2955
+
2956
+ # # # #--------------------Version 2.0 ----------------------------------------#
2957
+ # # # final_img, final_path, err = enhanced_face_swap_and_enhance(src_rgb, tgt_rgb)
2958
+ # # # #--------------------Version 2.0 ----------------------------------------#
2959
+
2960
+ # # if err:
2961
+ # # raise HTTPException(500, err)
2962
+
2963
+ # # with open(final_path, "rb") as f:
2964
+ # # result_bytes = f.read()
2965
+
2966
+ # # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
2967
+ # # result_url = upload_to_spaces(result_bytes, result_key)
2968
+ # # # -------------------------------------------------
2969
+ # # # COMPRESS IMAGE (2–3 MB target)
2970
+ # # # -------------------------------------------------
2971
+ # # compressed_bytes = compress_image(
2972
+ # # image_bytes=result_bytes,
2973
+ # # max_size=(1280, 1280),
2974
+ # # quality=72
2975
+ # # )
2976
+
2977
+ # # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
2978
+ # # compressed_url = upload_to_spaces(
2979
+ # # compressed_bytes,
2980
+ # # compressed_key,
2981
+ # # content_type="image/jpeg"
2982
+ # # )
2983
+ # # end_time = datetime.utcnow()
2984
+ # # response_time_ms = (end_time - start_time).total_seconds() * 1000
2985
+
2986
+ # # if database is not None:
2987
+ # # log_entry = {
2988
+ # # "endpoint": "/face-swap",
2989
+ # # "status": "success",
2990
+ # # "response_time_ms": response_time_ms,
2991
+ # # "timestamp": end_time
2992
+ # # }
2993
+ # # if appname:
2994
+ # # log_entry["appname"] = appname
2995
+ # # await database.api_logs.insert_one(log_entry)
2996
+
2997
+
2998
+ # # return {
2999
+ # # "result_key": result_key,
3000
+ # # "result_url": result_url,
3001
+ # # "Compressed_Image_URL": compressed_url
3002
+ # # }
3003
+
3004
+ # # except Exception as e:
3005
+ # # end_time = datetime.utcnow()
3006
+ # # response_time_ms = (end_time - start_time).total_seconds() * 1000
3007
+
3008
+ # # if database is not None:
3009
+ # # log_entry = {
3010
+ # # "endpoint": "/face-swap",
3011
+ # # "status": "fail",
3012
+ # # "response_time_ms": response_time_ms,
3013
+ # # "timestamp": end_time,
3014
+ # # "error": str(e)
3015
+ # # }
3016
+ # # if appname:
3017
+ # # log_entry["appname"] = appname
3018
+ # # await database.api_logs.insert_one(log_entry)
3019
+
3020
+ # # raise HTTPException(500, f"Face swap failed: {str(e)}")
3021
+ # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
3022
+ # async def face_swap_api(
3023
+ # source: UploadFile = File(...),
3024
+ # image2: Optional[UploadFile] = File(None),
3025
+ # target_category_id: str = Form(None),
3026
+ # new_category_id: str = Form(None),
3027
+ # user_id: Optional[str] = Form(None),
3028
+ # appname: Optional[str] = Form(None),
3029
+ # credentials: HTTPAuthorizationCredentials = Security(security)
3030
+ # ):
3031
+ # start_time = datetime.utcnow()
3032
+
3033
+ # try:
3034
+ # # ------------------------------------------------------------------
3035
+ # # VALIDATION
3036
+ # # ------------------------------------------------------------------
3037
+ # # --------------------------------------------------------------
3038
+ # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
3039
+ # # --------------------------------------------------------------
3040
+ # if target_category_id == "":
3041
+ # target_category_id = None
3042
+
3043
+ # if new_category_id == "":
3044
+ # new_category_id = None
3045
+
3046
+ # if user_id == "":
3047
+ # user_id = None
3048
+
3049
+ # # media_clicks_collection = get_media_clicks_collection(appname)
3050
+ # media_clicks_collection, subcategories_collection = get_app_db_collections(appname)
3051
+
3052
+
3053
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
3054
+
3055
+ # if target_category_id and new_category_id:
3056
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
3057
+
3058
+ # if not target_category_id and not new_category_id:
3059
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
3060
+
3061
+ # # ------------------------------------------------------------------
3062
+ # # READ SOURCE IMAGE
3063
+ # # ------------------------------------------------------------------
3064
+ # src_bytes = await source.read()
3065
+ # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
3066
+ # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
3067
+
3068
+ # # ------------------------------------------------------------------
3069
+ # # CASE 1 : new_category_id → MongoDB lookup
3070
+ # # ------------------------------------------------------------------
3071
+ # if new_category_id:
3072
+
3073
+ # # doc = await subcategories_col.find_one({
3074
+ # # "asset_images._id": ObjectId(new_category_id)
3075
+ # # })
3076
+ # doc = await subcategories_collection.find_one({
3077
+ # "asset_images._id": ObjectId(new_category_id)
3078
+ # })
3079
+
3080
+
3081
+ # if not doc:
3082
+ # raise HTTPException(404, "Asset image not found in database")
3083
+
3084
+ # # extract correct asset
3085
+ # asset = next(
3086
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
3087
+ # None
3088
+ # )
3089
+
3090
+ # if not asset:
3091
+ # raise HTTPException(404, "Asset image URL not found")
3092
+
3093
+ # # correct URL
3094
+ # target_url = asset["url"]
3095
+
3096
+ # # correct categoryId (ObjectId)
3097
+ # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
3098
+ # subcategory_oid = doc["_id"]
3099
+
3100
+ # # ------------------------------------------------------------------#
3101
+ # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
3102
+ # # ------------------------------------------------------------------#
3103
+ # if user_id and media_clicks_collection is not None:
3104
+ # try:
3105
+ # user_id_clean = user_id.strip()
3106
+ # if not user_id_clean:
3107
+ # raise ValueError("user_id cannot be empty")
3108
+ # try:
3109
+ # user_oid = ObjectId(user_id_clean)
3110
+ # except (InvalidId, ValueError) as e:
3111
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
3112
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
3113
+
3114
+ # now = datetime.utcnow()
3115
+
3116
+ # # Normalize dates (UTC midnight)
3117
+ # today_date = datetime(now.year, now.month, now.day)
3118
+
3119
+ # # -------------------------------------------------
3120
+ # # STEP 1: Ensure root document exists
3121
+ # # -------------------------------------------------
3122
+ # await media_clicks_collection.update_one(
3123
+ # {"userId": user_oid},
3124
+ # {
3125
+ # "$setOnInsert": {
3126
+ # "userId": user_oid,
3127
+ # "createdAt": now,
3128
+ # "ai_edit_complete": 0,
3129
+ # "ai_edit_daily_count": []
3130
+ # }
3131
+ # },
3132
+ # upsert=True
3133
+ # )
3134
+ # # -------------------------------------------------
3135
+ # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
3136
+ # # -------------------------------------------------
3137
+ # doc = await media_clicks_collection.find_one(
3138
+ # {"userId": user_oid},
3139
+ # {"ai_edit_daily_count": 1}
3140
+ # )
3141
+
3142
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
3143
+
3144
+ # # Normalize today to UTC midnight
3145
+ # today_date = datetime(now.year, now.month, now.day)
3146
+
3147
+ # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
3148
+ # daily_map = {}
3149
+ # for entry in daily_entries:
3150
+ # d = entry["date"]
3151
+ # if isinstance(d, datetime):
3152
+ # d = datetime(d.year, d.month, d.day)
3153
+ # daily_map[d] = entry["count"] # overwrite = no duplicates
3154
+
3155
+ # # Determine last recorded date
3156
+ # last_date = max(daily_map.keys()) if daily_map else today_date
3157
+
3158
+ # # Fill ALL missing days with count = 0
3159
+ # next_day = last_date + timedelta(days=1)
3160
+ # while next_day < today_date:
3161
+ # daily_map.setdefault(next_day, 0)
3162
+ # next_day += timedelta(days=1)
3163
+
3164
+ # # Mark today as used (binary)
3165
+ # daily_map[today_date] = 1
3166
+
3167
+ # # Rebuild list: OLDEST → NEWEST
3168
+ # final_daily_entries = [
3169
+ # {"date": d, "count": daily_map[d]}
3170
+ # for d in sorted(daily_map.keys())
3171
+ # ]
3172
+
3173
+ # # Keep only last 32 days
3174
+ # final_daily_entries = final_daily_entries[-32:]
3175
+
3176
+ # # Atomic replace
3177
+ # await media_clicks_collection.update_one(
3178
+ # {"userId": user_oid},
3179
+ # {
3180
+ # "$set": {
3181
+ # "ai_edit_daily_count": final_daily_entries,
3182
+ # "updatedAt": now
3183
+ # }
3184
+ # }
3185
+ # )
3186
+
3187
+ # # -------------------------------------------------
3188
+ # # STEP 3: Try updating existing subCategory
3189
+ # # -------------------------------------------------
3190
+ # update_result = await media_clicks_collection.update_one(
3191
+ # {
3192
+ # "userId": user_oid,
3193
+ # "subCategories.subCategoryId": subcategory_oid
3194
+ # },
3195
+ # {
3196
+ # "$inc": {
3197
+ # "subCategories.$.click_count": 1,
3198
+ # "ai_edit_complete": 1
3199
+ # },
3200
+ # "$set": {
3201
+ # "subCategories.$.lastClickedAt": now,
3202
+ # "ai_edit_last_date": now,
3203
+ # "updatedAt": now
3204
+ # }
3205
+ # }
3206
+ # )
3207
+
3208
+ # # -------------------------------------------------
3209
+ # # STEP 4: Push subCategory if missing
3210
+ # # -------------------------------------------------
3211
+ # if update_result.matched_count == 0:
3212
+ # await media_clicks_collection.update_one(
3213
+ # {"userId": user_oid},
3214
+ # {
3215
+ # "$inc": {
3216
+ # "ai_edit_complete": 1
3217
+ # },
3218
+ # "$set": {
3219
+ # "ai_edit_last_date": now,
3220
+ # "updatedAt": now
3221
+ # },
3222
+ # "$push": {
3223
+ # "subCategories": {
3224
+ # "subCategoryId": subcategory_oid,
3225
+ # "click_count": 1,
3226
+ # "lastClickedAt": now
3227
+ # }
3228
+ # }
3229
+ # }
3230
+ # )
3231
+
3232
+ # # -------------------------------------------------
3233
+ # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
3234
+ # # -------------------------------------------------
3235
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
3236
+ # if user_doc and "subCategories" in user_doc:
3237
+ # subcategories = user_doc["subCategories"]
3238
+ # # Sort by lastClickedAt in ascending order (oldest first)
3239
+ # # Handle missing or None dates by using datetime.min
3240
+ # subcategories_sorted = sorted(
3241
+ # subcategories,
3242
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
3243
+ # )
3244
+ # # Update with sorted array
3245
+ # await media_clicks_collection.update_one(
3246
+ # {"userId": user_oid},
3247
+ # {
3248
+ # "$set": {
3249
+ # "subCategories": subcategories_sorted,
3250
+ # "updatedAt": now
3251
+ # }
3252
+ # }
3253
+ # )
3254
+
3255
+ # logger.info(
3256
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
3257
+ # user_id,
3258
+ # str(subcategory_oid)
3259
+ # )
3260
+
3261
+ # except Exception as media_err:
3262
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
3263
+ # elif user_id and media_clicks_collection is None:
3264
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
3265
+
3266
+ # # # ------------------------------------------------------------------
3267
+ # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
3268
+ # # # ------------------------------------------------------------------
3269
+ # if target_category_id:
3270
+ # client = get_spaces_client()
3271
+ # base_prefix = "faceswap/target/"
3272
+ # resp = client.list_objects_v2(
3273
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
3274
+ # )
3275
+
3276
+ # # Extract categories from the CommonPrefixes
3277
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
3278
+
3279
+ # target_url = None
3280
+
3281
+ # # --- FIX STARTS HERE ---
3282
+ # for category in categories:
3283
+ # original_prefix = f"faceswap/target/{category}/original/"
3284
+ # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
3285
+
3286
+ # # List objects in original/
3287
+ # original_objects = client.list_objects_v2(
3288
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
3289
+ # ).get("Contents", [])
3290
+
3291
+ # # List objects in thumb/ (optional: for the old code's extra check)
3292
+ # thumb_objects = client.list_objects_v2(
3293
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
3294
+ # ).get("Contents", [])
3295
+
3296
+ # # Extract only the filenames and filter for .png
3297
+ # original_filenames = sorted([
3298
+ # obj["Key"].split("/")[-1] for obj in original_objects
3299
+ # if obj["Key"].split("/")[-1].endswith(".png")
3300
+ # ])
3301
+ # thumb_filenames = [
3302
+ # obj["Key"].split("/")[-1] for obj in thumb_objects
3303
+ # ]
3304
+
3305
+ # # Replicate the old indexing logic based on sorted filenames
3306
+ # for idx, filename in enumerate(original_filenames, start=1):
3307
+ # cid = f"{category.lower()}image_{idx}"
3308
+
3309
+ # # Optional: Replicate the thumb file check for 100% parity
3310
+ # # if filename in thumb_filenames and cid == target_category_id:
3311
+ # # Simpler check just on the ID, assuming thumb files are present
3312
+ # if cid == target_category_id:
3313
+ # # Construct the final target URL using the full prefix and the filename
3314
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
3315
+ # break
3316
+
3317
+ # if target_url:
3318
+ # break
3319
+ # # --- FIX ENDS HERE ---
3320
+
3321
+ # if not target_url:
3322
+ # raise HTTPException(404, "Target categoryId not found")
3323
+ # # # ------------------------------------------------------------------
3324
+ # # # DOWNLOAD TARGET IMAGE
3325
+ # # # ------------------------------------------------------------------
3326
+ # async with httpx.AsyncClient(timeout=30.0) as client:
3327
+ # response = await client.get(target_url)
3328
+ # response.raise_for_status()
3329
+ # tgt_bytes = response.content
3330
+
3331
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
3332
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
3333
+
3334
+ # if src_bgr is None or tgt_bgr is None:
3335
+ # raise HTTPException(400, "Invalid image data")
3336
+
3337
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
3338
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
3339
+
3340
+ # # ------------------------------------------------------------------
3341
+ # # READ OPTIONAL IMAGE2
3342
+ # # ------------------------------------------------------------------
3343
+ # img2_rgb = None
3344
+ # if image2:
3345
+ # img2_bytes = await image2.read()
3346
+ # img2_bgr = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
3347
+ # if img2_bgr is not None:
3348
+ # img2_rgb = cv2.cvtColor(img2_bgr, cv2.COLOR_BGR2RGB)
3349
+
3350
+ # # ------------------------------------------------------------------
3351
+ # # FACE SWAP EXECUTION (run in thread to not block event loop)
3352
+ # # ------------------------------------------------------------------
3353
+ # if img2_rgb is not None:
3354
+ # def _couple_swap():
3355
+ # pipeline_start = time.time()
3356
+ # src_images = [src_rgb, img2_rgb]
3357
+
3358
+ # all_src_faces = []
3359
+ # t0 = time.time()
3360
+ # for img in src_images:
3361
+ # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
3362
+ # all_src_faces.extend(faces)
3363
+
3364
+ # tgt_faces = face_analysis_app.get(cv2.cvtColor(tgt_rgb, cv2.COLOR_RGB2BGR))
3365
+ # logger.info(f"[Pipeline] Couple face detection: {time.time()-t0:.2f}s")
3366
+
3367
+ # if not all_src_faces:
3368
+ # raise ValueError("No faces detected in source images")
3369
+ # if not tgt_faces:
3370
+ # raise ValueError("No faces detected in target image")
3371
+
3372
+ # def face_sort_key(face):
3373
+ # x1, y1, x2, y2 = face.bbox
3374
+ # area = (x2 - x1) * (y2 - y1)
3375
+ # cx = (x1 + x2) / 2
3376
+ # return (-area, cx)
3377
+
3378
+ # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
3379
+ # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
3380
+ # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
3381
+ # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
3382
+
3383
+ # pairs = []
3384
+ # for s, t in zip(src_male, tgt_male):
3385
+ # pairs.append((s, t))
3386
+ # for s, t in zip(src_female, tgt_female):
3387
+ # pairs.append((s, t))
3388
+
3389
+ # if not pairs:
3390
+ # src_all = sorted(all_src_faces, key=face_sort_key)
3391
+ # tgt_all = sorted(tgt_faces, key=face_sort_key)
3392
+ # pairs = list(zip(src_all, tgt_all))
3393
+
3394
+ # t0 = time.time()
3395
+ # with swap_lock:
3396
+ # result_img = cv2.cvtColor(tgt_rgb, cv2.COLOR_RGB2BGR)
3397
+ # for src_face, _ in pairs:
3398
+ # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
3399
+ # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
3400
+ # target_face = candidates[0]
3401
+ # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
3402
+ # logger.info(f"[Pipeline] Couple face swap: {time.time()-t0:.2f}s")
3403
+
3404
+ # result_rgb_out = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
3405
+
3406
+ # t0 = time.time()
3407
+ # enhanced_rgb = mandatory_enhancement(result_rgb_out)
3408
+ # logger.info(f"[Pipeline] Couple enhancement: {time.time()-t0:.2f}s")
3409
+
3410
+ # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
3411
+
3412
+ # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
3413
+ # final_path = os.path.join(temp_dir, "result.png")
3414
+ # cv2.imwrite(final_path, enhanced_bgr)
3415
+
3416
+ # with open(final_path, "rb") as f:
3417
+ # result_bytes = f.read()
3418
+
3419
+ # logger.info(f"[Pipeline] TOTAL couple swap: {time.time()-pipeline_start:.2f}s")
3420
+ # return result_bytes
3421
+
3422
+ # try:
3423
+ # result_bytes = await asyncio.to_thread(_couple_swap)
3424
+ # except ValueError as ve:
3425
+ # raise HTTPException(400, str(ve))
3426
+
3427
+ # else:
3428
+ # # ----- SINGLE SOURCE SWAP (run in thread) -----
3429
+ # def _single_swap():
3430
+ # return face_swap_and_enhance(src_rgb, tgt_rgb)
3431
+
3432
+ # final_img, final_path, err = await asyncio.to_thread(_single_swap)
3433
+
3434
+ # if err:
3435
+ # raise HTTPException(500, err)
3436
+
3437
+ # with open(final_path, "rb") as f:
3438
+ # result_bytes = f.read()
3439
+
3440
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
3441
+ # result_url = upload_to_spaces(result_bytes, result_key)
3442
+ # # -------------------------------------------------
3443
+ # # COMPRESS IMAGE (2–3 MB target)
3444
+ # # -------------------------------------------------
3445
+ # compressed_bytes = compress_image(
3446
+ # image_bytes=result_bytes,
3447
+ # max_size=(1280, 1280),
3448
+ # quality=72
3449
+ # )
3450
+
3451
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
3452
+ # compressed_url = upload_to_spaces(
3453
+ # compressed_bytes,
3454
+ # compressed_key,
3455
+ # content_type="image/jpeg"
3456
+ # )
3457
+ # end_time = datetime.utcnow()
3458
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3459
+
3460
+ # if database is not None:
3461
+ # log_entry = {
3462
+ # "endpoint": "/face-swap",
3463
+ # "status": "success",
3464
+ # "response_time_ms": response_time_ms,
3465
+ # "timestamp": end_time
3466
+ # }
3467
+ # if appname:
3468
+ # log_entry["appname"] = appname
3469
+ # await database.api_logs.insert_one(log_entry)
3470
+
3471
+
3472
+ # return {
3473
+ # "result_key": result_key,
3474
+ # "result_url": result_url,
3475
+ # "Compressed_Image_URL": compressed_url
3476
+ # }
3477
+
3478
+ # except Exception as e:
3479
+ # end_time = datetime.utcnow()
3480
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3481
+
3482
+ # if database is not None:
3483
+ # log_entry = {
3484
+ # "endpoint": "/face-swap",
3485
+ # "status": "fail",
3486
+ # "response_time_ms": response_time_ms,
3487
+ # "timestamp": end_time,
3488
+ # "error": str(e)
3489
+ # }
3490
+ # if appname:
3491
+ # log_entry["appname"] = appname
3492
+ # await database.api_logs.insert_one(log_entry)
3493
+
3494
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
3495
+
3496
+ # @fastapi_app.get("/preview/{result_key:path}")
3497
+ # async def preview_result(result_key: str):
3498
+ # try:
3499
+ # img_bytes = download_from_spaces(result_key)
3500
+ # except Exception:
3501
+ # raise HTTPException(status_code=404, detail="Result not found")
3502
+ # return Response(
3503
+ # content=img_bytes,
3504
+ # media_type="image/png",
3505
+ # headers={"Content-Disposition": "inline; filename=result.png"}
3506
+ # )
3507
+
3508
+ # @fastapi_app.post("/multi-face-swap", dependencies=[Depends(verify_token)])
3509
+ # async def multi_face_swap_api(
3510
+ # source_image: UploadFile = File(...),
3511
+ # target_image: UploadFile = File(...)
3512
+ # ):
3513
+ # start_time = datetime.utcnow()
3514
+
3515
+ # try:
3516
+ # # -----------------------------
3517
+ # # Read images
3518
+ # # -----------------------------
3519
+ # src_bytes = await source_image.read()
3520
+ # tgt_bytes = await target_image.read()
3521
+
3522
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
3523
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
3524
+
3525
+ # if src_bgr is None or tgt_bgr is None:
3526
+ # raise HTTPException(400, "Invalid image data")
3527
+
3528
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
3529
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
3530
+
3531
+ # # -----------------------------
3532
+ # # Multi-face swap (run in thread to not block event loop)
3533
+ # # -----------------------------
3534
+ # def _multi_swap_and_enhance():
3535
+ # swapped_rgb = multi_face_swap(src_rgb, tgt_rgb)
3536
+ # return mandatory_enhancement(swapped_rgb)
3537
+
3538
+ # final_rgb = await asyncio.to_thread(_multi_swap_and_enhance)
3539
+
3540
+ # final_bgr = cv2.cvtColor(final_rgb, cv2.COLOR_RGB2BGR)
3541
+
3542
+ # # -----------------------------
3543
+ # # Save temp result
3544
+ # # -----------------------------
3545
+ # temp_dir = tempfile.mkdtemp(prefix="multi_faceswap_")
3546
+ # result_path = os.path.join(temp_dir, "result.png")
3547
+ # cv2.imwrite(result_path, final_bgr)
3548
+
3549
+ # with open(result_path, "rb") as f:
3550
+ # result_bytes = f.read()
3551
+
3552
+ # # -----------------------------
3553
+ # # Upload
3554
+ # # -----------------------------
3555
+ # result_key = f"faceswap/multi/{uuid.uuid4().hex}.png"
3556
+ # result_url = upload_to_spaces(
3557
+ # result_bytes,
3558
+ # result_key,
3559
+ # content_type="image/png"
3560
+ # )
3561
+
3562
+ # return {
3563
+ # "result_key": result_key,
3564
+ # "result_url": result_url
3565
+ # }
3566
+
3567
+ # except Exception as e:
3568
+ # raise HTTPException(status_code=500, detail=str(e))
3569
+
3570
+
3571
+ # @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
3572
+ # async def face_swap_couple_api(
3573
+ # image1: UploadFile = File(...),
3574
+ # image2: Optional[UploadFile] = File(None),
3575
+ # target_category_id: str = Form(None),
3576
+ # new_category_id: str = Form(None),
3577
+ # user_id: Optional[str] = Form(None),
3578
+ # appname: Optional[str] = Form(None),
3579
+ # credentials: HTTPAuthorizationCredentials = Security(security)
3580
+ # ):
3581
+ # """
3582
+ # Production-ready face swap endpoint supporting:
3583
+ # - Multiple source images (image1 + optional image2)
3584
+ # - Gender-based pairing
3585
+ # - Merged faces from multiple sources
3586
+ # - Mandatory CodeFormer enhancement
3587
+ # """
3588
+ # start_time = datetime.utcnow()
3589
+
3590
+ # try:
3591
+ # # -----------------------------
3592
+ # # Validate input
3593
+ # # -----------------------------
3594
+ # if target_category_id == "":
3595
+ # target_category_id = None
3596
+ # if new_category_id == "":
3597
+ # new_category_id = None
3598
+ # if user_id == "":
3599
+ # user_id = None
3600
+
3601
+ # media_clicks_collection = get_media_clicks_collection(appname)
3602
+
3603
+ # if target_category_id and new_category_id:
3604
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
3605
+ # if not target_category_id and not new_category_id:
3606
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
3607
+
3608
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
3609
+
3610
+ # # -----------------------------
3611
+ # # Read source images
3612
+ # # -----------------------------
3613
+ # src_images = []
3614
+ # img1_bytes = await image1.read()
3615
+ # src1 = cv2.imdecode(np.frombuffer(img1_bytes, np.uint8), cv2.IMREAD_COLOR)
3616
+ # if src1 is None:
3617
+ # raise HTTPException(400, "Invalid image1 data")
3618
+ # src_images.append(cv2.cvtColor(src1, cv2.COLOR_BGR2RGB))
3619
+
3620
+ # if image2:
3621
+ # img2_bytes = await image2.read()
3622
+ # src2 = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
3623
+ # if src2 is not None:
3624
+ # src_images.append(cv2.cvtColor(src2, cv2.COLOR_BGR2RGB))
3625
+
3626
+ # # -----------------------------
3627
+ # # Resolve target image
3628
+ # # -----------------------------
3629
+ # target_url = None
3630
+ # if new_category_id:
3631
+ # doc = await subcategories_col.find_one({
3632
+ # "asset_images._id": ObjectId(new_category_id)
3633
+ # })
3634
+
3635
+ # if not doc:
3636
+ # raise HTTPException(404, "Asset image not found in database")
3637
+
3638
+ # asset = next(
3639
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
3640
+ # None
3641
+ # )
3642
+
3643
+ # if not asset:
3644
+ # raise HTTPException(404, "Asset image URL not found")
3645
+
3646
+ # target_url = asset["url"]
3647
+ # subcategory_oid = doc["_id"]
3648
+
3649
+ # if user_id and media_clicks_collection is not None:
3650
+ # try:
3651
+ # user_id_clean = user_id.strip()
3652
+ # if not user_id_clean:
3653
+ # raise ValueError("user_id cannot be empty")
3654
+ # try:
3655
+ # user_oid = ObjectId(user_id_clean)
3656
+ # except (InvalidId, ValueError):
3657
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
3658
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
3659
+
3660
+ # now = datetime.utcnow()
3661
+
3662
+ # # Step 1: ensure root document exists
3663
+ # await media_clicks_collection.update_one(
3664
+ # {"userId": user_oid},
3665
+ # {
3666
+ # "$setOnInsert": {
3667
+ # "userId": user_oid,
3668
+ # "createdAt": now,
3669
+ # "ai_edit_complete": 0,
3670
+ # "ai_edit_daily_count": []
3671
+ # }
3672
+ # },
3673
+ # upsert=True
3674
+ # )
3675
+
3676
+ # # Step 2: handle daily usage (binary, no duplicates)
3677
+ # doc = await media_clicks_collection.find_one(
3678
+ # {"userId": user_oid},
3679
+ # {"ai_edit_daily_count": 1}
3680
+ # )
3681
+
3682
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
3683
+
3684
+ # today_date = datetime(now.year, now.month, now.day)
3685
+
3686
+ # daily_map = {}
3687
+ # for entry in daily_entries:
3688
+ # d = entry["date"]
3689
+ # if isinstance(d, datetime):
3690
+ # d = datetime(d.year, d.month, d.day)
3691
+ # daily_map[d] = entry["count"]
3692
+
3693
+ # last_date = max(daily_map.keys()) if daily_map else None
3694
+
3695
+ # if last_date != today_date:
3696
+ # daily_map[today_date] = 1
3697
+
3698
+ # final_daily_entries = [
3699
+ # {"date": d, "count": daily_map[d]}
3700
+ # for d in sorted(daily_map.keys())
3701
+ # ]
3702
+
3703
+ # final_daily_entries = final_daily_entries[-32:]
3704
+
3705
+ # await media_clicks_collection.update_one(
3706
+ # {"userId": user_oid},
3707
+ # {
3708
+ # "$set": {
3709
+ # "ai_edit_daily_count": final_daily_entries,
3710
+ # "updatedAt": now
3711
+ # }
3712
+ # }
3713
+ # )
3714
+
3715
+ # # Step 3: try updating existing subCategory
3716
+ # update_result = await media_clicks_collection.update_one(
3717
+ # {
3718
+ # "userId": user_oid,
3719
+ # "subCategories.subCategoryId": subcategory_oid
3720
+ # },
3721
+ # {
3722
+ # "$inc": {
3723
+ # "subCategories.$.click_count": 1,
3724
+ # "ai_edit_complete": 1
3725
+ # },
3726
+ # "$set": {
3727
+ # "subCategories.$.lastClickedAt": now,
3728
+ # "ai_edit_last_date": now,
3729
+ # "updatedAt": now
3730
+ # }
3731
+ # }
3732
+ # )
3733
+
3734
+ # # Step 4: push subCategory if missing
3735
+ # if update_result.matched_count == 0:
3736
+ # await media_clicks_collection.update_one(
3737
+ # {"userId": user_oid},
3738
+ # {
3739
+ # "$inc": {
3740
+ # "ai_edit_complete": 1
3741
+ # },
3742
+ # "$set": {
3743
+ # "ai_edit_last_date": now,
3744
+ # "updatedAt": now
3745
+ # },
3746
+ # "$push": {
3747
+ # "subCategories": {
3748
+ # "subCategoryId": subcategory_oid,
3749
+ # "click_count": 1,
3750
+ # "lastClickedAt": now
3751
+ # }
3752
+ # }
3753
+ # }
3754
+ # )
3755
+
3756
+ # # Step 5: sort subCategories by lastClickedAt (ascending)
3757
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
3758
+ # if user_doc and "subCategories" in user_doc:
3759
+ # subcategories = user_doc["subCategories"]
3760
+ # subcategories_sorted = sorted(
3761
+ # subcategories,
3762
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
3763
+ # )
3764
+ # await media_clicks_collection.update_one(
3765
+ # {"userId": user_oid},
3766
+ # {
3767
+ # "$set": {
3768
+ # "subCategories": subcategories_sorted,
3769
+ # "updatedAt": now
3770
+ # }
3771
+ # }
3772
+ # )
3773
+
3774
+ # logger.info(
3775
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
3776
+ # user_id,
3777
+ # str(subcategory_oid)
3778
+ # )
3779
+
3780
+ # except Exception as media_err:
3781
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
3782
+ # elif user_id and media_clicks_collection is None:
3783
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
3784
+
3785
+ # if target_category_id:
3786
+ # client = get_spaces_client()
3787
+ # base_prefix = "faceswap/target/"
3788
+ # resp = client.list_objects_v2(
3789
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
3790
+ # )
3791
+
3792
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
3793
+
3794
+ # for category in categories:
3795
+ # original_prefix = f"faceswap/target/{category}/original/"
3796
+ # thumb_prefix = f"faceswap/target/{category}/thumb/"
3797
+
3798
+ # original_objects = client.list_objects_v2(
3799
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
3800
+ # ).get("Contents", [])
3801
+
3802
+ # thumb_objects = client.list_objects_v2(
3803
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
3804
+ # ).get("Contents", [])
3805
+
3806
+ # original_filenames = sorted([
3807
+ # obj["Key"].split("/")[-1] for obj in original_objects
3808
+ # if obj["Key"].split("/")[-1].endswith(".png")
3809
+ # ])
3810
+
3811
+ # for idx, filename in enumerate(original_filenames, start=1):
3812
+ # cid = f"{category.lower()}image_{idx}"
3813
+ # if cid == target_category_id:
3814
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
3815
+ # break
3816
+
3817
+ # if target_url:
3818
+ # break
3819
+
3820
+ # if not target_url:
3821
+ # raise HTTPException(404, "Target categoryId not found")
3822
+
3823
+ # async with httpx.AsyncClient(timeout=30.0) as client:
3824
+ # response = await client.get(target_url)
3825
+ # response.raise_for_status()
3826
+ # tgt_bytes = response.content
3827
+
3828
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
3829
+ # if tgt_bgr is None:
3830
+ # raise HTTPException(400, "Invalid target image data")
3831
+
3832
+ # # -----------------------------
3833
+ # # Couple face swap + enhance (run in thread)
3834
+ # # -----------------------------
3835
+ # def _couple_face_swap_and_enhance():
3836
+ # pipeline_start = time.time()
3837
+
3838
+ # all_src_faces = []
3839
+ # t0 = time.time()
3840
+ # for img in src_images:
3841
+ # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
3842
+ # all_src_faces.extend(faces)
3843
+
3844
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
3845
+ # logger.info(f"[Pipeline] Couple-ep face detection: {time.time()-t0:.2f}s")
3846
+
3847
+ # if not all_src_faces:
3848
+ # raise ValueError("No faces detected in source images")
3849
+ # if not tgt_faces:
3850
+ # raise ValueError("No faces detected in target image")
3851
+
3852
+ # def face_sort_key(face):
3853
+ # x1, y1, x2, y2 = face.bbox
3854
+ # area = (x2 - x1) * (y2 - y1)
3855
+ # cx = (x1 + x2) / 2
3856
+ # return (-area, cx)
3857
+
3858
+ # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
3859
+ # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
3860
+ # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
3861
+ # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
3862
+
3863
+ # pairs = []
3864
+ # for s, t in zip(src_male, tgt_male):
3865
+ # pairs.append((s, t))
3866
+ # for s, t in zip(src_female, tgt_female):
3867
+ # pairs.append((s, t))
3868
+
3869
+ # if not pairs:
3870
+ # src_all = sorted(all_src_faces, key=face_sort_key)
3871
+ # tgt_all = sorted(tgt_faces, key=face_sort_key)
3872
+ # pairs = list(zip(src_all, tgt_all))
3873
+
3874
+ # t0 = time.time()
3875
+ # with swap_lock:
3876
+ # result_img = tgt_bgr.copy()
3877
+ # for src_face, _ in pairs:
3878
+ # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
3879
+ # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
3880
+ # target_face = candidates[0]
3881
+ # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
3882
+ # logger.info(f"[Pipeline] Couple-ep face swap: {time.time()-t0:.2f}s")
3883
+
3884
+ # result_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
3885
+
3886
+ # t0 = time.time()
3887
+ # enhanced_rgb = mandatory_enhancement(result_rgb)
3888
+ # logger.info(f"[Pipeline] Couple-ep enhancement: {time.time()-t0:.2f}s")
3889
+
3890
+ # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
3891
+
3892
+ # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
3893
+ # final_path = os.path.join(temp_dir, "result.png")
3894
+ # cv2.imwrite(final_path, enhanced_bgr)
3895
+
3896
+ # with open(final_path, "rb") as f:
3897
+ # result_bytes = f.read()
3898
+
3899
+ # logger.info(f"[Pipeline] TOTAL couple-ep swap: {time.time()-pipeline_start:.2f}s")
3900
+ # return result_bytes
3901
+
3902
+ # try:
3903
+ # result_bytes = await asyncio.to_thread(_couple_face_swap_and_enhance)
3904
+ # except ValueError as ve:
3905
+ # raise HTTPException(400, str(ve))
3906
+
3907
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
3908
+ # result_url = upload_to_spaces(result_bytes, result_key)
3909
+
3910
+ # compressed_bytes = compress_image(result_bytes, max_size=(1280, 1280), quality=72)
3911
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
3912
+ # compressed_url = upload_to_spaces(compressed_bytes, compressed_key, content_type="image/jpeg")
3913
+
3914
+ # # -----------------------------
3915
+ # # Log API usage
3916
+ # # -----------------------------
3917
+ # end_time = datetime.utcnow()
3918
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3919
+ # if database is not None:
3920
+ # log_entry = {
3921
+ # "endpoint": "/face-swap-couple",
3922
+ # "status": "success",
3923
+ # "response_time_ms": response_time_ms,
3924
+ # "timestamp": end_time
3925
+ # }
3926
+ # if appname:
3927
+ # log_entry["appname"] = appname
3928
+ # await database.api_logs.insert_one(log_entry)
3929
+
3930
+ # return {
3931
+ # "result_key": result_key,
3932
+ # "result_url": result_url,
3933
+ # "compressed_url": compressed_url
3934
+ # }
3935
+
3936
+ # except Exception as e:
3937
+ # end_time = datetime.utcnow()
3938
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3939
+ # if database is not None:
3940
+ # log_entry = {
3941
+ # "endpoint": "/face-swap-couple",
3942
+ # "status": "fail",
3943
+ # "response_time_ms": response_time_ms,
3944
+ # "timestamp": end_time,
3945
+ # "error": str(e)
3946
+ # }
3947
+ # if appname:
3948
+ # log_entry["appname"] = appname
3949
+ # await database.api_logs.insert_one(log_entry)
3950
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
3951
+
3952
+
3953
+ # if __name__ == "__main__":
3954
+ # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
3955
+
3956
+
3957
+
3958
+
3959
 
3960
 
3961