LogicGoInfotechSpaces commited on
Commit
53f4b57
·
verified ·
1 Parent(s): 2b071e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1431 -56
app.py CHANGED
@@ -2830,6 +2830,1434 @@
2830
  # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
2831
 
2832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2833
  import os
2834
  os.environ["OMP_NUM_THREADS"] = "1"
2835
  import shutil
@@ -2854,11 +4282,9 @@ from bson import ObjectId
2854
  from bson.errors import InvalidId
2855
  import httpx
2856
  import uvicorn
2857
- import gradio as gr
2858
- from gradio import mount_gradio_app
2859
  from PIL import Image
2860
  import io
2861
- # from scipy import ndimage
2862
  # DigitalOcean Spaces
2863
  import boto3
2864
  from botocore.client import Config
@@ -3069,19 +4495,6 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security))
3069
  return credentials.credentials
3070
 
3071
  # --------------------- DB Selector ---------------------
3072
- # def get_media_clicks_collection(appname: Optional[str] = None):
3073
- # """
3074
- # Returns the correct media_clicks collection based on appname.
3075
- # Defaults to the primary admin database when no appname is provided
3076
- # or when the requested database is unavailable.
3077
- # """
3078
- # if appname:
3079
- # normalized = appname.strip().lower()
3080
- # if normalized == "collage-maker":
3081
- # if collage_media_clicks_col is not None:
3082
- # return collage_media_clicks_col
3083
- # logger.warning("COLLAGE_MAKER_DB_URL not configured; falling back to default media_clicks collection")
3084
- # return media_clicks_col
3085
  def get_app_db_collections(appname: Optional[str] = None):
3086
  """
3087
  Returns (media_clicks_collection, subcategories_collection)
@@ -3325,30 +4738,6 @@ def download_from_spaces(key):
3325
  obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
3326
  return obj['Body'].read()
3327
 
3328
- def build_multi_faceswap_gradio():
3329
- with gr.Blocks() as demo:
3330
- gr.Markdown("## 👩‍❤️‍👨 Multi Face Swap (Couple → Couple)")
3331
-
3332
- with gr.Row():
3333
- src = gr.Image(type="numpy", label="Source Image (2 Faces)")
3334
- tgt = gr.Image(type="numpy", label="Target Image (2 Faces)")
3335
-
3336
- out = gr.Image(type="numpy", label="Swapped Result")
3337
- error = gr.Textbox(label="Logs", interactive=False)
3338
-
3339
- def process(src_img, tgt_img):
3340
- try:
3341
- swapped = multi_face_swap(src_img, tgt_img)
3342
- enhanced = enhance_image_with_codeformer(swapped)
3343
- return enhanced, ""
3344
- except Exception as e:
3345
- return None, str(e)
3346
-
3347
- btn = gr.Button("Swap Faces")
3348
- btn.click(process, [src, tgt], [out, error])
3349
-
3350
- return demo
3351
-
3352
  def mandatory_enhancement(rgb_img):
3353
  """
3354
  Always runs CodeFormer on the final image.
@@ -3377,8 +4766,6 @@ async def root():
3377
  async def health():
3378
  return {"status": "healthy"}
3379
 
3380
- from fastapi import Form
3381
- import requests
3382
  @fastapi_app.get("/test-admin-db")
3383
  async def test_admin_db():
3384
  try:
@@ -3853,7 +5240,7 @@ async def multi_face_swap_api(
3853
 
3854
 
3855
  @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
3856
- async def face_swap_api(
3857
  image1: UploadFile = File(...),
3858
  image2: Optional[UploadFile] = File(None),
3859
  target_category_id: str = Form(None),
@@ -4234,18 +5621,6 @@ async def face_swap_api(
4234
  raise HTTPException(500, f"Face swap failed: {str(e)}")
4235
 
4236
 
4237
-
4238
-
4239
- # --------------------- Mount Gradio ---------------------
4240
-
4241
- multi_faceswap_app = build_multi_faceswap_gradio()
4242
- fastapi_app = mount_gradio_app(
4243
- fastapi_app,
4244
- multi_faceswap_app,
4245
- path="/gradio-couple-faceswap"
4246
- )
4247
-
4248
-
4249
-
4250
  if __name__ == "__main__":
4251
  uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
 
 
2830
  # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
2831
 
2832
 
2833
+ # import os
2834
+ # os.environ["OMP_NUM_THREADS"] = "1"
2835
+ # import shutil
2836
+ # import uuid
2837
+ # import cv2
2838
+ # import numpy as np
2839
+ # import threading
2840
+ # import subprocess
2841
+ # import logging
2842
+ # import tempfile
2843
+ # import sys
2844
+ # from datetime import datetime,timedelta
2845
+ # import tempfile
2846
+ # import insightface
2847
+ # from insightface.app import FaceAnalysis
2848
+ # from huggingface_hub import hf_hub_download
2849
+ # from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
2850
+ # from fastapi.responses import RedirectResponse
2851
+ # from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
2852
+ # from motor.motor_asyncio import AsyncIOMotorClient
2853
+ # from bson import ObjectId
2854
+ # from bson.errors import InvalidId
2855
+ # import httpx
2856
+ # import uvicorn
2857
+ # import gradio as gr
2858
+ # from gradio import mount_gradio_app
2859
+ # from PIL import Image
2860
+ # import io
2861
+ # # from scipy import ndimage
2862
+ # # DigitalOcean Spaces
2863
+ # import boto3
2864
+ # from botocore.client import Config
2865
+ # from typing import Optional
2866
+
2867
+ # # --------------------- Logging ---------------------
2868
+ # logging.basicConfig(level=logging.INFO)
2869
+ # logger = logging.getLogger(__name__)
2870
+
2871
+ # # --------------------- Secrets & Paths ---------------------
2872
+ # REPO_ID = "HariLogicgo/face_swap_models"
2873
+ # MODELS_DIR = "./models"
2874
+ # os.makedirs(MODELS_DIR, exist_ok=True)
2875
+
2876
+ # HF_TOKEN = os.getenv("HF_TOKEN")
2877
+ # API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN")
2878
+
2879
+ # DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
2880
+ # DO_SPACES_ENDPOINT = f"https://{DO_SPACES_REGION}.digitaloceanspaces.com"
2881
+ # DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
2882
+ # DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
2883
+ # DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
2884
+
2885
+ # # NEW admin DB (with error handling for missing env vars)
2886
+ # ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
2887
+ # admin_client = None
2888
+ # admin_db = None
2889
+ # subcategories_col = None
2890
+ # media_clicks_col = None
2891
+ # if ADMIN_MONGO_URL:
2892
+ # try:
2893
+ # admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
2894
+ # admin_db = admin_client.adminPanel
2895
+ # subcategories_col = admin_db.subcategories
2896
+ # media_clicks_col = admin_db.media_clicks
2897
+ # except Exception as e:
2898
+ # logger.warning(f"MongoDB admin connection failed (optional): {e}")
2899
+
2900
+ # # Collage Maker DB (optional)
2901
+ # COLLAGE_MAKER_DB_URL = os.getenv("COLLAGE_MAKER_DB_URL")
2902
+ # collage_maker_client = None
2903
+ # collage_maker_db = None
2904
+ # collage_media_clicks_col = None
2905
+ # collage_subcategories_col = None
2906
+ # if COLLAGE_MAKER_DB_URL:
2907
+ # try:
2908
+ # collage_maker_client = AsyncIOMotorClient(COLLAGE_MAKER_DB_URL)
2909
+ # collage_maker_db = collage_maker_client.adminPanel
2910
+ # collage_media_clicks_col = collage_maker_db.media_clicks
2911
+ # collage_subcategories_col = collage_maker_db.subcategories
2912
+ # except Exception as e:
2913
+ # logger.warning(f"MongoDB collage-maker connection failed (optional): {e}")
2914
+
2915
+ # # AI Enhancer DB (optional)
2916
+
2917
+ # AI_ENHANCER_DB_URL = os.getenv("AI_ENHANCER_DB_URL")
2918
+ # ai_enhancer_client = None
2919
+ # ai_enhancer_db = None
2920
+ # ai_enhancer_media_clicks_col = None
2921
+ # ai_enhancer_subcategories_col = None
2922
+
2923
+ # if AI_ENHANCER_DB_URL:
2924
+ # try:
2925
+ # ai_enhancer_client = AsyncIOMotorClient(AI_ENHANCER_DB_URL)
2926
+ # ai_enhancer_db = ai_enhancer_client.test # 🔴 test database
2927
+ # ai_enhancer_media_clicks_col = ai_enhancer_db.media_clicks
2928
+ # ai_enhancer_subcategories_col = ai_enhancer_db.subcategories
2929
+ # except Exception as e:
2930
+ # logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
2931
+
2932
+
2933
+ # def get_media_clicks_collection(appname: Optional[str] = None):
2934
+ # """Return the media clicks collection for the given app (default: main admin)."""
2935
+ # if appname and str(appname).strip().lower() == "collage-maker":
2936
+ # return collage_media_clicks_col
2937
+ # return media_clicks_col
2938
+
2939
+
2940
+ # # OLD logs DB
2941
+ # MONGODB_URL = os.getenv("MONGODB_URL")
2942
+ # client = None
2943
+ # database = None
2944
+
2945
+ # # --------------------- Download Models ---------------------
2946
+ # def download_models():
2947
+ # try:
2948
+ # logger.info("Downloading models...")
2949
+ # inswapper_path = hf_hub_download(
2950
+ # repo_id=REPO_ID,
2951
+ # filename="models/inswapper_128.onnx",
2952
+ # repo_type="model",
2953
+ # local_dir=MODELS_DIR,
2954
+ # token=HF_TOKEN
2955
+ # )
2956
+
2957
+ # buffalo_files = ["1k3d68.onnx", "2d106det.onnx", "genderage.onnx", "det_10g.onnx", "w600k_r50.onnx"]
2958
+ # for f in buffalo_files:
2959
+ # hf_hub_download(
2960
+ # repo_id=REPO_ID,
2961
+ # filename=f"models/buffalo_l/" + f,
2962
+ # repo_type="model",
2963
+ # local_dir=MODELS_DIR,
2964
+ # token=HF_TOKEN
2965
+ # )
2966
+
2967
+ # logger.info("Models downloaded successfully.")
2968
+ # return inswapper_path
2969
+ # except Exception as e:
2970
+ # logger.error(f"Model download failed: {e}")
2971
+ # raise
2972
+
2973
+ # try:
2974
+ # inswapper_path = download_models()
2975
+
2976
+ # # --------------------- Face Analysis + Swapper ---------------------
2977
+ # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
2978
+ # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
2979
+ # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
2980
+ # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
2981
+ # logger.info("Face analysis models loaded successfully")
2982
+ # except Exception as e:
2983
+ # logger.error(f"Failed to initialize face analysis models: {e}")
2984
+ # # Set defaults to prevent crash
2985
+ # inswapper_path = None
2986
+ # face_analysis_app = None
2987
+ # swapper = None
2988
+
2989
+ # # --------------------- CodeFormer ---------------------
2990
+ # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
2991
+
2992
+ # def ensure_codeformer():
2993
+ # try:
2994
+ # if not os.path.exists("CodeFormer"):
2995
+ # logger.info("CodeFormer not found, cloning repository...")
2996
+ # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
2997
+ # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=False) # Non-critical deps
2998
+
2999
+ # # Always ensure BasicSR is installed from local directory
3000
+ # # This is needed for Hugging Face Spaces where BasicSR can't be installed from GitHub
3001
+ # if os.path.exists("CodeFormer/basicsr/setup.py"):
3002
+ # logger.info("Installing BasicSR from local directory...")
3003
+ # subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
3004
+ # logger.info("BasicSR installed successfully")
3005
+
3006
+ # # Install realesrgan after BasicSR is installed (realesrgan depends on BasicSR)
3007
+ # # This must be done after BasicSR installation to avoid PyPI install issues
3008
+ # try:
3009
+ # import realesrgan
3010
+ # logger.info("RealESRGAN already installed")
3011
+ # except ImportError:
3012
+ # logger.info("Installing RealESRGAN...")
3013
+ # subprocess.run("pip install --no-cache-dir realesrgan", shell=True, check=True)
3014
+ # logger.info("RealESRGAN installed successfully")
3015
+
3016
+ # # Download models if CodeFormer exists (fixed logic)
3017
+ # if os.path.exists("CodeFormer"):
3018
+ # try:
3019
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=False, timeout=300)
3020
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
3021
+ # logger.warning("Failed to download facelib models (optional)")
3022
+ # try:
3023
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=False, timeout=300)
3024
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
3025
+ # logger.warning("Failed to download CodeFormer models (optional)")
3026
+ # except Exception as e:
3027
+ # logger.error(f"CodeFormer setup failed: {e}")
3028
+ # logger.warning("Continuing without CodeFormer features...")
3029
+
3030
+ # ensure_codeformer()
3031
+ # # --------------------- FastAPI ---------------------
3032
+ # fastapi_app = FastAPI()
3033
+
3034
+ # @fastapi_app.on_event("startup")
3035
+ # async def startup_db():
3036
+ # global client, database
3037
+ # if MONGODB_URL:
3038
+ # try:
3039
+ # logger.info("Initializing MongoDB for API logs...")
3040
+ # client = AsyncIOMotorClient(MONGODB_URL)
3041
+ # database = client.FaceSwap
3042
+ # logger.info("MongoDB initialized for API logs")
3043
+ # except Exception as e:
3044
+ # logger.warning(f"MongoDB connection failed (optional): {e}")
3045
+ # client = None
3046
+ # database = None
3047
+ # else:
3048
+ # logger.warning("MONGODB_URL not set, skipping MongoDB initialization")
3049
+
3050
+ # @fastapi_app.on_event("shutdown")
3051
+ # async def shutdown_db():
3052
+ # global client, admin_client, collage_maker_client
3053
+ # if client is not None:
3054
+ # client.close()
3055
+ # logger.info("MongoDB connection closed")
3056
+ # if admin_client is not None:
3057
+ # admin_client.close()
3058
+ # logger.info("Admin MongoDB connection closed")
3059
+ # if collage_maker_client is not None:
3060
+ # collage_maker_client.close()
3061
+ # logger.info("Collage Maker MongoDB connection closed")
3062
+
3063
+ # # --------------------- Auth ---------------------
3064
+ # security = HTTPBearer()
3065
+
3066
+ # def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
3067
+ # if credentials.credentials != API_SECRET_TOKEN:
3068
+ # raise HTTPException(status_code=401, detail="Invalid or missing token")
3069
+ # return credentials.credentials
3070
+
3071
+ # # --------------------- DB Selector ---------------------
3072
+ # # def get_media_clicks_collection(appname: Optional[str] = None):
3073
+ # # """
3074
+ # # Returns the correct media_clicks collection based on appname.
3075
+ # # Defaults to the primary admin database when no appname is provided
3076
+ # # or when the requested database is unavailable.
3077
+ # # """
3078
+ # # if appname:
3079
+ # # normalized = appname.strip().lower()
3080
+ # # if normalized == "collage-maker":
3081
+ # # if collage_media_clicks_col is not None:
3082
+ # # return collage_media_clicks_col
3083
+ # # logger.warning("COLLAGE_MAKER_DB_URL not configured; falling back to default media_clicks collection")
3084
+ # # return media_clicks_col
3085
+ # def get_app_db_collections(appname: Optional[str] = None):
3086
+ # """
3087
+ # Returns (media_clicks_collection, subcategories_collection)
3088
+ # based on appname.
3089
+ # """
3090
+
3091
+ # if appname:
3092
+ # app = appname.strip().lower()
3093
+
3094
+ # if app == "collage-maker":
3095
+ # if collage_media_clicks_col is not None and collage_subcategories_col is not None:
3096
+ # return collage_media_clicks_col, collage_subcategories_col
3097
+ # logger.warning("Collage-maker DB not configured, falling back to admin")
3098
+
3099
+ # elif app == "ai-enhancer":
3100
+ # if ai_enhancer_media_clicks_col is not None and ai_enhancer_subcategories_col is not None:
3101
+ # return ai_enhancer_media_clicks_col, ai_enhancer_subcategories_col
3102
+ # logger.warning("AI-Enhancer DB not configured, falling back to admin")
3103
+
3104
+ # # default fallback
3105
+ # return media_clicks_col, subcategories_col
3106
+
3107
+
3108
+
3109
+ # # --------------------- Logging API Hits ---------------------
3110
+ # async def log_faceswap_hit(token: str, status: str = "success"):
3111
+ # global database
3112
+ # if database is None:
3113
+ # return
3114
+ # await database.api_logs.insert_one({
3115
+ # "token": token,
3116
+ # "endpoint": "/faceswap",
3117
+ # "status": status,
3118
+ # "timestamp": datetime.utcnow()
3119
+ # })
3120
+
3121
+ # # --------------------- Face Swap Pipeline ---------------------
3122
+ # swap_lock = threading.Lock()
3123
+
3124
+ # def enhance_image_with_codeformer(rgb_img, temp_dir=None):
3125
+ # if temp_dir is None:
3126
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"enhance_{uuid.uuid4().hex[:8]}")
3127
+ # os.makedirs(temp_dir, exist_ok=True)
3128
+
3129
+ # input_path = os.path.join(temp_dir, "input.jpg")
3130
+ # cv2.imwrite(input_path, cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR))
3131
+
3132
+ # python_cmd = sys.executable if sys.executable else "python3"
3133
+ # cmd = (
3134
+ # f"{python_cmd} {CODEFORMER_PATH} "
3135
+ # f"-w 0.7 "
3136
+ # f"--input_path {input_path} "
3137
+ # f"--output_path {temp_dir} "
3138
+ # f"--bg_upsampler realesrgan "
3139
+ # f"--face_upsample"
3140
+ # )
3141
+
3142
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
3143
+ # if result.returncode != 0:
3144
+ # raise RuntimeError(result.stderr)
3145
+
3146
+ # final_dir = os.path.join(temp_dir, "final_results")
3147
+ # files = [f for f in os.listdir(final_dir) if f.endswith(".png")]
3148
+ # if not files:
3149
+ # raise RuntimeError("No enhanced output")
3150
+
3151
+ # final_path = os.path.join(final_dir, files[0])
3152
+ # enhanced = cv2.imread(final_path)
3153
+ # return cv2.cvtColor(enhanced, cv2.COLOR_BGR2RGB)
3154
+
3155
+ # def multi_face_swap(src_img, tgt_img):
3156
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
3157
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
3158
+
3159
+ # src_faces = face_analysis_app.get(src_bgr)
3160
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
3161
+
3162
+ # if not src_faces or not tgt_faces:
3163
+ # raise ValueError("No faces detected")
3164
+
3165
+ # def face_sort_key(face):
3166
+ # x1, y1, x2, y2 = face.bbox
3167
+ # area = (x2 - x1) * (y2 - y1)
3168
+ # cx = (x1 + x2) / 2
3169
+ # return (-area, cx)
3170
+
3171
+ # # Split by gender
3172
+ # src_male = [f for f in src_faces if f.gender == 1]
3173
+ # src_female = [f for f in src_faces if f.gender == 0]
3174
+
3175
+ # tgt_male = [f for f in tgt_faces if f.gender == 1]
3176
+ # tgt_female = [f for f in tgt_faces if f.gender == 0]
3177
+
3178
+ # # Sort inside gender groups
3179
+ # src_male = sorted(src_male, key=face_sort_key)
3180
+ # src_female = sorted(src_female, key=face_sort_key)
3181
+
3182
+ # tgt_male = sorted(tgt_male, key=face_sort_key)
3183
+ # tgt_female = sorted(tgt_female, key=face_sort_key)
3184
+
3185
+ # # Build final swap pairs
3186
+ # pairs = []
3187
+
3188
+ # for s, t in zip(src_male, tgt_male):
3189
+ # pairs.append((s, t))
3190
+
3191
+ # for s, t in zip(src_female, tgt_female):
3192
+ # pairs.append((s, t))
3193
+
3194
+ # # Fallback if gender mismatch
3195
+ # if not pairs:
3196
+ # src_faces = sorted(src_faces, key=face_sort_key)
3197
+ # tgt_faces = sorted(tgt_faces, key=face_sort_key)
3198
+ # pairs = list(zip(src_faces, tgt_faces))
3199
+
3200
+ # result_img = tgt_bgr.copy()
3201
+
3202
+ # for src_face, _ in pairs:
3203
+ # # 🔁 re-detect current target faces
3204
+ # if face_analysis_app is None:
3205
+ # raise ValueError("Face analysis models not initialized. Please ensure models are downloaded.")
3206
+ # current_faces = face_analysis_app.get(result_img)
3207
+ # current_faces = sorted(current_faces, key=face_sort_key)
3208
+
3209
+ # # choose best matching gender
3210
+ # candidates = [
3211
+ # f for f in current_faces if f.gender == src_face.gender
3212
+ # ] or current_faces
3213
+
3214
+ # target_face = candidates[0]
3215
+
3216
+ # if swapper is None:
3217
+ # raise ValueError("Face swap models not initialized. Please ensure models are downloaded.")
3218
+ # result_img = swapper.get(
3219
+ # result_img,
3220
+ # target_face,
3221
+ # src_face,
3222
+ # paste_back=True
3223
+ # )
3224
+
3225
+ # return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
3226
+
3227
+
3228
+
3229
+ # def face_swap_and_enhance(src_img, tgt_img, temp_dir=None):
3230
+ # try:
3231
+ # with swap_lock:
3232
+ # # Use a temp dir for intermediate files
3233
+ # if temp_dir is None:
3234
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_work_{uuid.uuid4().hex[:8]}")
3235
+ # if os.path.exists(temp_dir):
3236
+ # shutil.rmtree(temp_dir)
3237
+ # os.makedirs(temp_dir, exist_ok=True)
3238
+
3239
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
3240
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
3241
+
3242
+ # src_faces = face_analysis_app.get(src_bgr)
3243
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
3244
+ # if face_analysis_app is None:
3245
+ # return None, None, "❌ Face analysis models not initialized. Please ensure models are downloaded."
3246
+ # if not src_faces or not tgt_faces:
3247
+ # return None, None, "❌ Face not detected in one of the images"
3248
+
3249
+ # swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
3250
+ # if swapper is None:
3251
+ # return None, None, "❌ Face swap models not initialized. Please ensure models are downloaded."
3252
+ # swapped_bgr = swapper.get(tgt_bgr, tgt_faces[0], src_faces[0])
3253
+ # if swapped_bgr is None:
3254
+ # return None, None, "❌ Face swap failed"
3255
+
3256
+ # cv2.imwrite(swapped_path, swapped_bgr)
3257
+
3258
+ # python_cmd = sys.executable if sys.executable else "python3"
3259
+ # cmd = f"{python_cmd} {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
3260
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
3261
+ # if result.returncode != 0:
3262
+ # return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
3263
+
3264
+ # final_results_dir = os.path.join(temp_dir, "final_results")
3265
+ # final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
3266
+ # if not final_files:
3267
+ # return None, None, "❌ No enhanced image found"
3268
+
3269
+ # final_path = os.path.join(final_results_dir, final_files[0])
3270
+ # final_img_bgr = cv2.imread(final_path)
3271
+ # if final_img_bgr is None:
3272
+ # return None, None, "❌ Failed to read enhanced image file"
3273
+ # final_img = cv2.cvtColor(final_img_bgr, cv2.COLOR_BGR2RGB)
3274
+
3275
+ # return final_img, final_path, ""
3276
+
3277
+ # except Exception as e:
3278
+ # return None, None, f"❌ Error: {str(e)}"
3279
+
3280
+ # def compress_image(
3281
+ # image_bytes: bytes,
3282
+ # max_size=(1280, 1280), # max width/height
3283
+ # quality=75 # JPEG quality (60–80 is ideal)
3284
+ # ) -> bytes:
3285
+ # """
3286
+ # Compress image by resizing and lowering quality.
3287
+ # Returns compressed image bytes.
3288
+ # """
3289
+ # img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
3290
+
3291
+ # # Resize while maintaining aspect ratio
3292
+ # img.thumbnail(max_size, Image.LANCZOS)
3293
+
3294
+ # output = io.BytesIO()
3295
+ # img.save(
3296
+ # output,
3297
+ # format="JPEG",
3298
+ # quality=quality,
3299
+ # optimize=True,
3300
+ # progressive=True
3301
+ # )
3302
+
3303
+ # return output.getvalue()
3304
+
3305
+ # # --------------------- DigitalOcean Spaces Helper ---------------------
3306
+ # def get_spaces_client():
3307
+ # session = boto3.session.Session()
3308
+ # client = session.client(
3309
+ # 's3',
3310
+ # region_name=DO_SPACES_REGION,
3311
+ # endpoint_url=DO_SPACES_ENDPOINT,
3312
+ # aws_access_key_id=DO_SPACES_KEY,
3313
+ # aws_secret_access_key=DO_SPACES_SECRET,
3314
+ # config=Config(signature_version='s3v4')
3315
+ # )
3316
+ # return client
3317
+
3318
+ # def upload_to_spaces(file_bytes, key, content_type="image/png"):
3319
+ # client = get_spaces_client()
3320
+ # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
3321
+ # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
3322
+
3323
+ # def download_from_spaces(key):
3324
+ # client = get_spaces_client()
3325
+ # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
3326
+ # return obj['Body'].read()
3327
+
3328
+ # def build_multi_faceswap_gradio():
3329
+ # with gr.Blocks() as demo:
3330
+ # gr.Markdown("## 👩‍❤️‍👨 Multi Face Swap (Couple → Couple)")
3331
+
3332
+ # with gr.Row():
3333
+ # src = gr.Image(type="numpy", label="Source Image (2 Faces)")
3334
+ # tgt = gr.Image(type="numpy", label="Target Image (2 Faces)")
3335
+
3336
+ # out = gr.Image(type="numpy", label="Swapped Result")
3337
+ # error = gr.Textbox(label="Logs", interactive=False)
3338
+
3339
+ # def process(src_img, tgt_img):
3340
+ # try:
3341
+ # swapped = multi_face_swap(src_img, tgt_img)
3342
+ # enhanced = enhance_image_with_codeformer(swapped)
3343
+ # return enhanced, ""
3344
+ # except Exception as e:
3345
+ # return None, str(e)
3346
+
3347
+ # btn = gr.Button("Swap Faces")
3348
+ # btn.click(process, [src, tgt], [out, error])
3349
+
3350
+ # return demo
3351
+
3352
+ # def mandatory_enhancement(rgb_img):
3353
+ # """
3354
+ # Always runs CodeFormer on the final image.
3355
+ # Fail-safe: returns original if enhancement fails.
3356
+ # """
3357
+ # try:
3358
+ # return enhance_image_with_codeformer(rgb_img)
3359
+ # except Exception as e:
3360
+ # logger.error(f"CodeFormer failed, returning original: {e}")
3361
+ # return rgb_img
3362
+
3363
+ # # --------------------- API Endpoints ---------------------
3364
+ # @fastapi_app.get("/")
3365
+ # async def root():
3366
+ # """Root endpoint"""
3367
+ # return {
3368
+ # "success": True,
3369
+ # "message": "FaceSwap API",
3370
+ # "data": {
3371
+ # "version": "1.0.0",
3372
+ # "Product Name":"Beauty Camera - GlowCam AI Studio",
3373
+ # "Released By" : "LogicGo Infotech"
3374
+ # }
3375
+ # }
3376
+ # @fastapi_app.get("/health")
3377
+ # async def health():
3378
+ # return {"status": "healthy"}
3379
+
3380
+ # from fastapi import Form
3381
+ # import requests
3382
+ # @fastapi_app.get("/test-admin-db")
3383
+ # async def test_admin_db():
3384
+ # try:
3385
+ # doc = await admin_db.list_collection_names()
3386
+ # return {"ok": True, "collections": doc}
3387
+ # except Exception as e:
3388
+ # return {"ok": False, "error": str(e), "url": ADMIN_MONGO_URL}
3389
+
3390
+ # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
3391
+ # async def face_swap_api(
3392
+ # source: UploadFile = File(...),
3393
+ # target_category_id: str = Form(None),
3394
+ # new_category_id: str = Form(None),
3395
+ # user_id: Optional[str] = Form(None),
3396
+ # appname: Optional[str] = Form(None),
3397
+ # credentials: HTTPAuthorizationCredentials = Security(security)
3398
+ # ):
3399
+ # start_time = datetime.utcnow()
3400
+
3401
+ # try:
3402
+ # # ------------------------------------------------------------------
3403
+ # # VALIDATION
3404
+ # # ------------------------------------------------------------------
3405
+ # # --------------------------------------------------------------
3406
+ # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
3407
+ # # --------------------------------------------------------------
3408
+ # if target_category_id == "":
3409
+ # target_category_id = None
3410
+
3411
+ # if new_category_id == "":
3412
+ # new_category_id = None
3413
+
3414
+ # if user_id == "":
3415
+ # user_id = None
3416
+
3417
+ # # media_clicks_collection = get_media_clicks_collection(appname)
3418
+ # media_clicks_collection, subcategories_collection = get_app_db_collections(appname)
3419
+
3420
+
3421
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
3422
+
3423
+ # if target_category_id and new_category_id:
3424
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
3425
+
3426
+ # if not target_category_id and not new_category_id:
3427
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
3428
+
3429
+ # # ------------------------------------------------------------------
3430
+ # # READ SOURCE IMAGE
3431
+ # # ------------------------------------------------------------------
3432
+ # src_bytes = await source.read()
3433
+ # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
3434
+ # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
3435
+
3436
+ # # ------------------------------------------------------------------
3437
+ # # CASE 1 : new_category_id → MongoDB lookup
3438
+ # # ------------------------------------------------------------------
3439
+ # if new_category_id:
3440
+
3441
+ # # doc = await subcategories_col.find_one({
3442
+ # # "asset_images._id": ObjectId(new_category_id)
3443
+ # # })
3444
+ # doc = await subcategories_collection.find_one({
3445
+ # "asset_images._id": ObjectId(new_category_id)
3446
+ # })
3447
+
3448
+
3449
+ # if not doc:
3450
+ # raise HTTPException(404, "Asset image not found in database")
3451
+
3452
+ # # extract correct asset
3453
+ # asset = next(
3454
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
3455
+ # None
3456
+ # )
3457
+
3458
+ # if not asset:
3459
+ # raise HTTPException(404, "Asset image URL not found")
3460
+
3461
+ # # correct URL
3462
+ # target_url = asset["url"]
3463
+
3464
+ # # correct categoryId (ObjectId)
3465
+ # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
3466
+ # subcategory_oid = doc["_id"]
3467
+
3468
+ # # ------------------------------------------------------------------#
3469
+ # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
3470
+ # # ------------------------------------------------------------------#
3471
+ # if user_id and media_clicks_collection is not None:
3472
+ # try:
3473
+ # user_id_clean = user_id.strip()
3474
+ # if not user_id_clean:
3475
+ # raise ValueError("user_id cannot be empty")
3476
+ # try:
3477
+ # user_oid = ObjectId(user_id_clean)
3478
+ # except (InvalidId, ValueError) as e:
3479
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
3480
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
3481
+
3482
+ # now = datetime.utcnow()
3483
+
3484
+ # # Normalize dates (UTC midnight)
3485
+ # today_date = datetime(now.year, now.month, now.day)
3486
+
3487
+ # # -------------------------------------------------
3488
+ # # STEP 1: Ensure root document exists
3489
+ # # -------------------------------------------------
3490
+ # await media_clicks_collection.update_one(
3491
+ # {"userId": user_oid},
3492
+ # {
3493
+ # "$setOnInsert": {
3494
+ # "userId": user_oid,
3495
+ # "createdAt": now,
3496
+ # "ai_edit_complete": 0,
3497
+ # "ai_edit_daily_count": []
3498
+ # }
3499
+ # },
3500
+ # upsert=True
3501
+ # )
3502
+ # # -------------------------------------------------
3503
+ # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
3504
+ # # -------------------------------------------------
3505
+ # doc = await media_clicks_collection.find_one(
3506
+ # {"userId": user_oid},
3507
+ # {"ai_edit_daily_count": 1}
3508
+ # )
3509
+
3510
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
3511
+
3512
+ # # Normalize today to UTC midnight
3513
+ # today_date = datetime(now.year, now.month, now.day)
3514
+
3515
+ # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
3516
+ # daily_map = {}
3517
+ # for entry in daily_entries:
3518
+ # d = entry["date"]
3519
+ # if isinstance(d, datetime):
3520
+ # d = datetime(d.year, d.month, d.day)
3521
+ # daily_map[d] = entry["count"] # overwrite = no duplicates
3522
+
3523
+ # # Determine last recorded date
3524
+ # last_date = max(daily_map.keys()) if daily_map else today_date
3525
+
3526
+ # # Fill ALL missing days with count = 0
3527
+ # next_day = last_date + timedelta(days=1)
3528
+ # while next_day < today_date:
3529
+ # daily_map.setdefault(next_day, 0)
3530
+ # next_day += timedelta(days=1)
3531
+
3532
+ # # Mark today as used (binary)
3533
+ # daily_map[today_date] = 1
3534
+
3535
+ # # Rebuild list: OLDEST → NEWEST
3536
+ # final_daily_entries = [
3537
+ # {"date": d, "count": daily_map[d]}
3538
+ # for d in sorted(daily_map.keys())
3539
+ # ]
3540
+
3541
+ # # Keep only last 32 days
3542
+ # final_daily_entries = final_daily_entries[-32:]
3543
+
3544
+ # # Atomic replace
3545
+ # await media_clicks_collection.update_one(
3546
+ # {"userId": user_oid},
3547
+ # {
3548
+ # "$set": {
3549
+ # "ai_edit_daily_count": final_daily_entries,
3550
+ # "updatedAt": now
3551
+ # }
3552
+ # }
3553
+ # )
3554
+
3555
+ # # -------------------------------------------------
3556
+ # # STEP 3: Try updating existing subCategory
3557
+ # # -------------------------------------------------
3558
+ # update_result = await media_clicks_collection.update_one(
3559
+ # {
3560
+ # "userId": user_oid,
3561
+ # "subCategories.subCategoryId": subcategory_oid
3562
+ # },
3563
+ # {
3564
+ # "$inc": {
3565
+ # "subCategories.$.click_count": 1,
3566
+ # "ai_edit_complete": 1
3567
+ # },
3568
+ # "$set": {
3569
+ # "subCategories.$.lastClickedAt": now,
3570
+ # "ai_edit_last_date": now,
3571
+ # "updatedAt": now
3572
+ # }
3573
+ # }
3574
+ # )
3575
+
3576
+ # # -------------------------------------------------
3577
+ # # STEP 4: Push subCategory if missing
3578
+ # # -------------------------------------------------
3579
+ # if update_result.matched_count == 0:
3580
+ # await media_clicks_collection.update_one(
3581
+ # {"userId": user_oid},
3582
+ # {
3583
+ # "$inc": {
3584
+ # "ai_edit_complete": 1
3585
+ # },
3586
+ # "$set": {
3587
+ # "ai_edit_last_date": now,
3588
+ # "updatedAt": now
3589
+ # },
3590
+ # "$push": {
3591
+ # "subCategories": {
3592
+ # "subCategoryId": subcategory_oid,
3593
+ # "click_count": 1,
3594
+ # "lastClickedAt": now
3595
+ # }
3596
+ # }
3597
+ # }
3598
+ # )
3599
+
3600
+ # # -------------------------------------------------
3601
+ # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
3602
+ # # -------------------------------------------------
3603
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
3604
+ # if user_doc and "subCategories" in user_doc:
3605
+ # subcategories = user_doc["subCategories"]
3606
+ # # Sort by lastClickedAt in ascending order (oldest first)
3607
+ # # Handle missing or None dates by using datetime.min
3608
+ # subcategories_sorted = sorted(
3609
+ # subcategories,
3610
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
3611
+ # )
3612
+ # # Update with sorted array
3613
+ # await media_clicks_collection.update_one(
3614
+ # {"userId": user_oid},
3615
+ # {
3616
+ # "$set": {
3617
+ # "subCategories": subcategories_sorted,
3618
+ # "updatedAt": now
3619
+ # }
3620
+ # }
3621
+ # )
3622
+
3623
+ # logger.info(
3624
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
3625
+ # user_id,
3626
+ # str(subcategory_oid)
3627
+ # )
3628
+
3629
+ # except Exception as media_err:
3630
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
3631
+ # elif user_id and media_clicks_collection is None:
3632
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
3633
+
3634
+ # # # ------------------------------------------------------------------
3635
+ # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
3636
+ # # # ------------------------------------------------------------------
3637
+ # if target_category_id:
3638
+ # client = get_spaces_client()
3639
+ # base_prefix = "faceswap/target/"
3640
+ # resp = client.list_objects_v2(
3641
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
3642
+ # )
3643
+
3644
+ # # Extract categories from the CommonPrefixes
3645
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
3646
+
3647
+ # target_url = None
3648
+
3649
+ # # --- FIX STARTS HERE ---
3650
+ # for category in categories:
3651
+ # original_prefix = f"faceswap/target/{category}/original/"
3652
+ # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
3653
+
3654
+ # # List objects in original/
3655
+ # original_objects = client.list_objects_v2(
3656
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
3657
+ # ).get("Contents", [])
3658
+
3659
+ # # List objects in thumb/ (optional: for the old code's extra check)
3660
+ # thumb_objects = client.list_objects_v2(
3661
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
3662
+ # ).get("Contents", [])
3663
+
3664
+ # # Extract only the filenames and filter for .png
3665
+ # original_filenames = sorted([
3666
+ # obj["Key"].split("/")[-1] for obj in original_objects
3667
+ # if obj["Key"].split("/")[-1].endswith(".png")
3668
+ # ])
3669
+ # thumb_filenames = [
3670
+ # obj["Key"].split("/")[-1] for obj in thumb_objects
3671
+ # ]
3672
+
3673
+ # # Replicate the old indexing logic based on sorted filenames
3674
+ # for idx, filename in enumerate(original_filenames, start=1):
3675
+ # cid = f"{category.lower()}image_{idx}"
3676
+
3677
+ # # Optional: Replicate the thumb file check for 100% parity
3678
+ # # if filename in thumb_filenames and cid == target_category_id:
3679
+ # # Simpler check just on the ID, assuming thumb files are present
3680
+ # if cid == target_category_id:
3681
+ # # Construct the final target URL using the full prefix and the filename
3682
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
3683
+ # break
3684
+
3685
+ # if target_url:
3686
+ # break
3687
+ # # --- FIX ENDS HERE ---
3688
+
3689
+ # if not target_url:
3690
+ # raise HTTPException(404, "Target categoryId not found")
3691
+ # # # ------------------------------------------------------------------
3692
+ # # # DOWNLOAD TARGET IMAGE
3693
+ # # # ------------------------------------------------------------------
3694
+ # async with httpx.AsyncClient(timeout=30.0) as client:
3695
+ # response = await client.get(target_url)
3696
+ # response.raise_for_status()
3697
+ # tgt_bytes = response.content
3698
+
3699
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
3700
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
3701
+
3702
+ # if src_bgr is None or tgt_bgr is None:
3703
+ # raise HTTPException(400, "Invalid image data")
3704
+
3705
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
3706
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
3707
+
3708
+ # # ------------------------------------------------------------------
3709
+ # # FACE SWAP EXECUTION
3710
+ # # ------------------------------------------------------------------
3711
+ # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
3712
+
3713
+ # # #--------------------Version 2.0 ----------------------------------------#
3714
+ # # final_img, final_path, err = enhanced_face_swap_and_enhance(src_rgb, tgt_rgb)
3715
+ # # #--------------------Version 2.0 ----------------------------------------#
3716
+
3717
+ # if err:
3718
+ # raise HTTPException(500, err)
3719
+
3720
+ # with open(final_path, "rb") as f:
3721
+ # result_bytes = f.read()
3722
+
3723
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
3724
+ # result_url = upload_to_spaces(result_bytes, result_key)
3725
+ # # -------------------------------------------------
3726
+ # # COMPRESS IMAGE (2–3 MB target)
3727
+ # # -------------------------------------------------
3728
+ # compressed_bytes = compress_image(
3729
+ # image_bytes=result_bytes,
3730
+ # max_size=(1280, 1280),
3731
+ # quality=72
3732
+ # )
3733
+
3734
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
3735
+ # compressed_url = upload_to_spaces(
3736
+ # compressed_bytes,
3737
+ # compressed_key,
3738
+ # content_type="image/jpeg"
3739
+ # )
3740
+ # end_time = datetime.utcnow()
3741
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3742
+
3743
+ # if database is not None:
3744
+ # log_entry = {
3745
+ # "endpoint": "/face-swap",
3746
+ # "status": "success",
3747
+ # "response_time_ms": response_time_ms,
3748
+ # "timestamp": end_time
3749
+ # }
3750
+ # if appname:
3751
+ # log_entry["appname"] = appname
3752
+ # await database.api_logs.insert_one(log_entry)
3753
+
3754
+
3755
+ # return {
3756
+ # "result_key": result_key,
3757
+ # "result_url": result_url,
3758
+ # "Compressed_Image_URL": compressed_url
3759
+ # }
3760
+
3761
+ # except Exception as e:
3762
+ # end_time = datetime.utcnow()
3763
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
3764
+
3765
+ # if database is not None:
3766
+ # log_entry = {
3767
+ # "endpoint": "/face-swap",
3768
+ # "status": "fail",
3769
+ # "response_time_ms": response_time_ms,
3770
+ # "timestamp": end_time,
3771
+ # "error": str(e)
3772
+ # }
3773
+ # if appname:
3774
+ # log_entry["appname"] = appname
3775
+ # await database.api_logs.insert_one(log_entry)
3776
+
3777
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
3778
+
3779
+ # @fastapi_app.get("/preview/{result_key:path}")
3780
+ # async def preview_result(result_key: str):
3781
+ # try:
3782
+ # img_bytes = download_from_spaces(result_key)
3783
+ # except Exception:
3784
+ # raise HTTPException(status_code=404, detail="Result not found")
3785
+ # return Response(
3786
+ # content=img_bytes,
3787
+ # media_type="image/png",
3788
+ # headers={"Content-Disposition": "inline; filename=result.png"}
3789
+ # )
3790
+
3791
+ # @fastapi_app.post("/multi-face-swap", dependencies=[Depends(verify_token)])
3792
+ # async def multi_face_swap_api(
3793
+ # source_image: UploadFile = File(...),
3794
+ # target_image: UploadFile = File(...)
3795
+ # ):
3796
+ # start_time = datetime.utcnow()
3797
+
3798
+ # try:
3799
+ # # -----------------------------
3800
+ # # Read images
3801
+ # # -----------------------------
3802
+ # src_bytes = await source_image.read()
3803
+ # tgt_bytes = await target_image.read()
3804
+
3805
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
3806
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
3807
+
3808
+ # if src_bgr is None or tgt_bgr is None:
3809
+ # raise HTTPException(400, "Invalid image data")
3810
+
3811
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
3812
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
3813
+
3814
+ # # -----------------------------
3815
+ # # Multi-face swap
3816
+ # # -----------------------------
3817
+ # swapped_rgb = multi_face_swap(src_rgb, tgt_rgb)
3818
+
3819
+ # # -----------------------------
3820
+ # # 🔥 MANDATORY ENHANCEMENT
3821
+ # # -----------------------------
3822
+ # final_rgb = mandatory_enhancement(swapped_rgb)
3823
+
3824
+ # final_bgr = cv2.cvtColor(final_rgb, cv2.COLOR_RGB2BGR)
3825
+
3826
+ # # -----------------------------
3827
+ # # Save temp result
3828
+ # # -----------------------------
3829
+ # temp_dir = tempfile.mkdtemp(prefix="multi_faceswap_")
3830
+ # result_path = os.path.join(temp_dir, "result.png")
3831
+ # cv2.imwrite(result_path, final_bgr)
3832
+
3833
+ # with open(result_path, "rb") as f:
3834
+ # result_bytes = f.read()
3835
+
3836
+ # # -----------------------------
3837
+ # # Upload
3838
+ # # -----------------------------
3839
+ # result_key = f"faceswap/multi/{uuid.uuid4().hex}.png"
3840
+ # result_url = upload_to_spaces(
3841
+ # result_bytes,
3842
+ # result_key,
3843
+ # content_type="image/png"
3844
+ # )
3845
+
3846
+ # return {
3847
+ # "result_key": result_key,
3848
+ # "result_url": result_url
3849
+ # }
3850
+
3851
+ # except Exception as e:
3852
+ # raise HTTPException(status_code=500, detail=str(e))
3853
+
3854
+
3855
+ # @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
3856
+ # async def face_swap_api(
3857
+ # image1: UploadFile = File(...),
3858
+ # image2: Optional[UploadFile] = File(None),
3859
+ # target_category_id: str = Form(None),
3860
+ # new_category_id: str = Form(None),
3861
+ # user_id: Optional[str] = Form(None),
3862
+ # appname: Optional[str] = Form(None),
3863
+ # credentials: HTTPAuthorizationCredentials = Security(security)
3864
+ # ):
3865
+ # """
3866
+ # Production-ready face swap endpoint supporting:
3867
+ # - Multiple source images (image1 + optional image2)
3868
+ # - Gender-based pairing
3869
+ # - Merged faces from multiple sources
3870
+ # - Mandatory CodeFormer enhancement
3871
+ # """
3872
+ # start_time = datetime.utcnow()
3873
+
3874
+ # try:
3875
+ # # -----------------------------
3876
+ # # Validate input
3877
+ # # -----------------------------
3878
+ # if target_category_id == "":
3879
+ # target_category_id = None
3880
+ # if new_category_id == "":
3881
+ # new_category_id = None
3882
+ # if user_id == "":
3883
+ # user_id = None
3884
+
3885
+ # media_clicks_collection = get_media_clicks_collection(appname)
3886
+
3887
+ # if target_category_id and new_category_id:
3888
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
3889
+ # if not target_category_id and not new_category_id:
3890
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
3891
+
3892
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
3893
+
3894
+ # # -----------------------------
3895
+ # # Read source images
3896
+ # # -----------------------------
3897
+ # src_images = []
3898
+ # img1_bytes = await image1.read()
3899
+ # src1 = cv2.imdecode(np.frombuffer(img1_bytes, np.uint8), cv2.IMREAD_COLOR)
3900
+ # if src1 is None:
3901
+ # raise HTTPException(400, "Invalid image1 data")
3902
+ # src_images.append(cv2.cvtColor(src1, cv2.COLOR_BGR2RGB))
3903
+
3904
+ # if image2:
3905
+ # img2_bytes = await image2.read()
3906
+ # src2 = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
3907
+ # if src2 is not None:
3908
+ # src_images.append(cv2.cvtColor(src2, cv2.COLOR_BGR2RGB))
3909
+
3910
+ # # -----------------------------
3911
+ # # Resolve target image
3912
+ # # -----------------------------
3913
+ # target_url = None
3914
+ # if new_category_id:
3915
+ # doc = await subcategories_col.find_one({
3916
+ # "asset_images._id": ObjectId(new_category_id)
3917
+ # })
3918
+
3919
+ # if not doc:
3920
+ # raise HTTPException(404, "Asset image not found in database")
3921
+
3922
+ # asset = next(
3923
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
3924
+ # None
3925
+ # )
3926
+
3927
+ # if not asset:
3928
+ # raise HTTPException(404, "Asset image URL not found")
3929
+
3930
+ # target_url = asset["url"]
3931
+ # subcategory_oid = doc["_id"]
3932
+
3933
+ # if user_id and media_clicks_collection is not None:
3934
+ # try:
3935
+ # user_id_clean = user_id.strip()
3936
+ # if not user_id_clean:
3937
+ # raise ValueError("user_id cannot be empty")
3938
+ # try:
3939
+ # user_oid = ObjectId(user_id_clean)
3940
+ # except (InvalidId, ValueError):
3941
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
3942
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
3943
+
3944
+ # now = datetime.utcnow()
3945
+
3946
+ # # Step 1: ensure root document exists
3947
+ # await media_clicks_collection.update_one(
3948
+ # {"userId": user_oid},
3949
+ # {
3950
+ # "$setOnInsert": {
3951
+ # "userId": user_oid,
3952
+ # "createdAt": now,
3953
+ # "ai_edit_complete": 0,
3954
+ # "ai_edit_daily_count": []
3955
+ # }
3956
+ # },
3957
+ # upsert=True
3958
+ # )
3959
+
3960
+ # # Step 2: handle daily usage (binary, no duplicates)
3961
+ # doc = await media_clicks_collection.find_one(
3962
+ # {"userId": user_oid},
3963
+ # {"ai_edit_daily_count": 1}
3964
+ # )
3965
+
3966
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
3967
+
3968
+ # today_date = datetime(now.year, now.month, now.day)
3969
+
3970
+ # daily_map = {}
3971
+ # for entry in daily_entries:
3972
+ # d = entry["date"]
3973
+ # if isinstance(d, datetime):
3974
+ # d = datetime(d.year, d.month, d.day)
3975
+ # daily_map[d] = entry["count"]
3976
+
3977
+ # last_date = max(daily_map.keys()) if daily_map else None
3978
+
3979
+ # if last_date != today_date:
3980
+ # daily_map[today_date] = 1
3981
+
3982
+ # final_daily_entries = [
3983
+ # {"date": d, "count": daily_map[d]}
3984
+ # for d in sorted(daily_map.keys())
3985
+ # ]
3986
+
3987
+ # final_daily_entries = final_daily_entries[-32:]
3988
+
3989
+ # await media_clicks_collection.update_one(
3990
+ # {"userId": user_oid},
3991
+ # {
3992
+ # "$set": {
3993
+ # "ai_edit_daily_count": final_daily_entries,
3994
+ # "updatedAt": now
3995
+ # }
3996
+ # }
3997
+ # )
3998
+
3999
+ # # Step 3: try updating existing subCategory
4000
+ # update_result = await media_clicks_collection.update_one(
4001
+ # {
4002
+ # "userId": user_oid,
4003
+ # "subCategories.subCategoryId": subcategory_oid
4004
+ # },
4005
+ # {
4006
+ # "$inc": {
4007
+ # "subCategories.$.click_count": 1,
4008
+ # "ai_edit_complete": 1
4009
+ # },
4010
+ # "$set": {
4011
+ # "subCategories.$.lastClickedAt": now,
4012
+ # "ai_edit_last_date": now,
4013
+ # "updatedAt": now
4014
+ # }
4015
+ # }
4016
+ # )
4017
+
4018
+ # # Step 4: push subCategory if missing
4019
+ # if update_result.matched_count == 0:
4020
+ # await media_clicks_collection.update_one(
4021
+ # {"userId": user_oid},
4022
+ # {
4023
+ # "$inc": {
4024
+ # "ai_edit_complete": 1
4025
+ # },
4026
+ # "$set": {
4027
+ # "ai_edit_last_date": now,
4028
+ # "updatedAt": now
4029
+ # },
4030
+ # "$push": {
4031
+ # "subCategories": {
4032
+ # "subCategoryId": subcategory_oid,
4033
+ # "click_count": 1,
4034
+ # "lastClickedAt": now
4035
+ # }
4036
+ # }
4037
+ # }
4038
+ # )
4039
+
4040
+ # # Step 5: sort subCategories by lastClickedAt (ascending)
4041
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
4042
+ # if user_doc and "subCategories" in user_doc:
4043
+ # subcategories = user_doc["subCategories"]
4044
+ # subcategories_sorted = sorted(
4045
+ # subcategories,
4046
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
4047
+ # )
4048
+ # await media_clicks_collection.update_one(
4049
+ # {"userId": user_oid},
4050
+ # {
4051
+ # "$set": {
4052
+ # "subCategories": subcategories_sorted,
4053
+ # "updatedAt": now
4054
+ # }
4055
+ # }
4056
+ # )
4057
+
4058
+ # logger.info(
4059
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
4060
+ # user_id,
4061
+ # str(subcategory_oid)
4062
+ # )
4063
+
4064
+ # except Exception as media_err:
4065
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
4066
+ # elif user_id and media_clicks_collection is None:
4067
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
4068
+
4069
+ # if target_category_id:
4070
+ # client = get_spaces_client()
4071
+ # base_prefix = "faceswap/target/"
4072
+ # resp = client.list_objects_v2(
4073
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
4074
+ # )
4075
+
4076
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
4077
+
4078
+ # for category in categories:
4079
+ # original_prefix = f"faceswap/target/{category}/original/"
4080
+ # thumb_prefix = f"faceswap/target/{category}/thumb/"
4081
+
4082
+ # original_objects = client.list_objects_v2(
4083
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
4084
+ # ).get("Contents", [])
4085
+
4086
+ # thumb_objects = client.list_objects_v2(
4087
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
4088
+ # ).get("Contents", [])
4089
+
4090
+ # original_filenames = sorted([
4091
+ # obj["Key"].split("/")[-1] for obj in original_objects
4092
+ # if obj["Key"].split("/")[-1].endswith(".png")
4093
+ # ])
4094
+
4095
+ # for idx, filename in enumerate(original_filenames, start=1):
4096
+ # cid = f"{category.lower()}image_{idx}"
4097
+ # if cid == target_category_id:
4098
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
4099
+ # break
4100
+
4101
+ # if target_url:
4102
+ # break
4103
+
4104
+ # if not target_url:
4105
+ # raise HTTPException(404, "Target categoryId not found")
4106
+
4107
+ # async with httpx.AsyncClient(timeout=30.0) as client:
4108
+ # response = await client.get(target_url)
4109
+ # response.raise_for_status()
4110
+ # tgt_bytes = response.content
4111
+
4112
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
4113
+ # if tgt_bgr is None:
4114
+ # raise HTTPException(400, "Invalid target image data")
4115
+
4116
+ # # -----------------------------
4117
+ # # Merge all source faces
4118
+ # # -----------------------------
4119
+ # all_src_faces = []
4120
+ # for img in src_images:
4121
+ # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
4122
+ # all_src_faces.extend(faces)
4123
+
4124
+ # if not all_src_faces:
4125
+ # raise HTTPException(400, "No faces detected in source images")
4126
+
4127
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
4128
+ # if not tgt_faces:
4129
+ # raise HTTPException(400, "No faces detected in target image")
4130
+
4131
+ # # -----------------------------
4132
+ # # Gender-based pairing
4133
+ # # -----------------------------
4134
+ # def face_sort_key(face):
4135
+ # x1, y1, x2, y2 = face.bbox
4136
+ # area = (x2 - x1) * (y2 - y1)
4137
+ # cx = (x1 + x2) / 2
4138
+ # return (-area, cx)
4139
+
4140
+ # # Separate by gender
4141
+ # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
4142
+ # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
4143
+ # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
4144
+ # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
4145
+
4146
+ # pairs = []
4147
+ # for s, t in zip(src_male, tgt_male):
4148
+ # pairs.append((s, t))
4149
+ # for s, t in zip(src_female, tgt_female):
4150
+ # pairs.append((s, t))
4151
+
4152
+ # # fallback if gender mismatch
4153
+ # if not pairs:
4154
+ # src_all = sorted(all_src_faces, key=face_sort_key)
4155
+ # tgt_all = sorted(tgt_faces, key=face_sort_key)
4156
+ # pairs = list(zip(src_all, tgt_all))
4157
+
4158
+ # # -----------------------------
4159
+ # # Perform face swap
4160
+ # # -----------------------------
4161
+ # with swap_lock:
4162
+ # result_img = tgt_bgr.copy()
4163
+ # for src_face, _ in pairs:
4164
+ # if face_analysis_app is None:
4165
+ # raise HTTPException(status_code=500, detail="Face analysis models not initialized. Please ensure models are downloaded.")
4166
+ # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
4167
+ # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
4168
+ # target_face = candidates[0]
4169
+ # if swapper is None:
4170
+ # raise HTTPException(status_code=500, detail="Face swap models not initialized. Please ensure models are downloaded.")
4171
+ # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
4172
+
4173
+ # result_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
4174
+
4175
+ # # -----------------------------
4176
+ # # Mandatory enhancement
4177
+ # # -----------------------------
4178
+ # enhanced_rgb = mandatory_enhancement(result_rgb)
4179
+ # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
4180
+
4181
+ # # -----------------------------
4182
+ # # Save, upload, compress
4183
+ # # -----------------------------
4184
+ # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
4185
+ # final_path = os.path.join(temp_dir, "result.png")
4186
+ # cv2.imwrite(final_path, enhanced_bgr)
4187
+
4188
+ # with open(final_path, "rb") as f:
4189
+ # result_bytes = f.read()
4190
+
4191
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
4192
+ # result_url = upload_to_spaces(result_bytes, result_key)
4193
+
4194
+ # compressed_bytes = compress_image(result_bytes, max_size=(1280, 1280), quality=72)
4195
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
4196
+ # compressed_url = upload_to_spaces(compressed_bytes, compressed_key, content_type="image/jpeg")
4197
+
4198
+ # # -----------------------------
4199
+ # # Log API usage
4200
+ # # -----------------------------
4201
+ # end_time = datetime.utcnow()
4202
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
4203
+ # if database is not None:
4204
+ # log_entry = {
4205
+ # "endpoint": "/face-swap-couple",
4206
+ # "status": "success",
4207
+ # "response_time_ms": response_time_ms,
4208
+ # "timestamp": end_time
4209
+ # }
4210
+ # if appname:
4211
+ # log_entry["appname"] = appname
4212
+ # await database.api_logs.insert_one(log_entry)
4213
+
4214
+ # return {
4215
+ # "result_key": result_key,
4216
+ # "result_url": result_url,
4217
+ # "compressed_url": compressed_url
4218
+ # }
4219
+
4220
+ # except Exception as e:
4221
+ # end_time = datetime.utcnow()
4222
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
4223
+ # if database is not None:
4224
+ # log_entry = {
4225
+ # "endpoint": "/face-swap-couple",
4226
+ # "status": "fail",
4227
+ # "response_time_ms": response_time_ms,
4228
+ # "timestamp": end_time,
4229
+ # "error": str(e)
4230
+ # }
4231
+ # if appname:
4232
+ # log_entry["appname"] = appname
4233
+ # await database.api_logs.insert_one(log_entry)
4234
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
4235
+
4236
+
4237
+
4238
+
4239
+ # # --------------------- Mount Gradio ---------------------
4240
+
4241
+ # multi_faceswap_app = build_multi_faceswap_gradio()
4242
+ # fastapi_app = mount_gradio_app(
4243
+ # fastapi_app,
4244
+ # multi_faceswap_app,
4245
+ # path="/gradio-couple-faceswap"
4246
+ # )
4247
+
4248
+
4249
+
4250
+ # if __name__ == "__main__":
4251
+ # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
4252
+
4253
+
4254
+
4255
+
4256
+
4257
+
4258
+
4259
+
4260
+ #####################FASTAPI___________________##############
4261
  import os
4262
  os.environ["OMP_NUM_THREADS"] = "1"
4263
  import shutil
 
4282
  from bson.errors import InvalidId
4283
  import httpx
4284
  import uvicorn
 
 
4285
  from PIL import Image
4286
  import io
4287
+ import requests
4288
  # DigitalOcean Spaces
4289
  import boto3
4290
  from botocore.client import Config
 
4495
  return credentials.credentials
4496
 
4497
  # --------------------- DB Selector ---------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
4498
  def get_app_db_collections(appname: Optional[str] = None):
4499
  """
4500
  Returns (media_clicks_collection, subcategories_collection)
 
4738
  obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
4739
  return obj['Body'].read()
4740
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4741
  def mandatory_enhancement(rgb_img):
4742
  """
4743
  Always runs CodeFormer on the final image.
 
4766
  async def health():
4767
  return {"status": "healthy"}
4768
 
 
 
4769
  @fastapi_app.get("/test-admin-db")
4770
  async def test_admin_db():
4771
  try:
 
5240
 
5241
 
5242
  @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
5243
+ async def face_swap_couple_api(
5244
  image1: UploadFile = File(...),
5245
  image2: Optional[UploadFile] = File(None),
5246
  target_category_id: str = Form(None),
 
5621
  raise HTTPException(500, f"Face swap failed: {str(e)}")
5622
 
5623
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5624
  if __name__ == "__main__":
5625
  uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
5626
+