LogicGoInfotechSpaces commited on
Commit
7e05725
·
verified ·
1 Parent(s): 4733998

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1420 -0
app.py CHANGED
@@ -1,3 +1,1415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # --------------------- List Images Endpoint ---------------------
2
  import os
3
  os.environ["OMP_NUM_THREADS"] = "1"
@@ -96,6 +1508,14 @@ if AI_ENHANCER_DB_URL:
96
  except Exception as e:
97
  logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
98
 
 
 
 
 
 
 
 
 
99
  # OLD logs DB
100
  MONGODB_URL = os.getenv("MONGODB_URL")
101
  client = None
 
1
+ # # --------------------- List Images Endpoint ---------------------
2
+ # import os
3
+ # os.environ["OMP_NUM_THREADS"] = "1"
4
+ # import shutil
5
+ # import uuid
6
+ # import cv2
7
+ # import numpy as np
8
+ # import threading
9
+ # import subprocess
10
+ # import logging
11
+ # import tempfile
12
+ # import sys
13
+ # from datetime import datetime,timedelta
14
+ # import tempfile
15
+ # import insightface
16
+ # from insightface.app import FaceAnalysis
17
+ # from huggingface_hub import hf_hub_download
18
+ # from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
19
+ # from fastapi.responses import RedirectResponse
20
+ # from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
21
+ # from motor.motor_asyncio import AsyncIOMotorClient
22
+ # from bson import ObjectId
23
+ # from bson.errors import InvalidId
24
+ # import httpx
25
+ # import uvicorn
26
+ # import gradio as gr
27
+ # from gradio import mount_gradio_app
28
+ # from PIL import Image
29
+ # import io
30
+ # # from scipy import ndimage
31
+ # # DigitalOcean Spaces
32
+ # import boto3
33
+ # from botocore.client import Config
34
+ # from typing import Optional
35
+
36
+ # # --------------------- Logging ---------------------
37
+ # logging.basicConfig(level=logging.INFO)
38
+ # logger = logging.getLogger(__name__)
39
+
40
+ # # --------------------- Secrets & Paths ---------------------
41
+ # REPO_ID = "HariLogicgo/face_swap_models"
42
+ # MODELS_DIR = "./models"
43
+ # os.makedirs(MODELS_DIR, exist_ok=True)
44
+
45
+ # HF_TOKEN = os.getenv("HF_TOKEN")
46
+ # API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN")
47
+
48
+ # DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
49
+ # DO_SPACES_ENDPOINT = f"https://{DO_SPACES_REGION}.digitaloceanspaces.com"
50
+ # DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
51
+ # DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
52
+ # DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
53
+
54
+ # # NEW admin DB (with error handling for missing env vars)
55
+ # ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
56
+ # admin_client = None
57
+ # admin_db = None
58
+ # subcategories_col = None
59
+ # media_clicks_col = None
60
+ # if ADMIN_MONGO_URL:
61
+ # try:
62
+ # admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
63
+ # admin_db = admin_client.adminPanel
64
+ # subcategories_col = admin_db.subcategories
65
+ # media_clicks_col = admin_db.media_clicks
66
+ # except Exception as e:
67
+ # logger.warning(f"MongoDB admin connection failed (optional): {e}")
68
+
69
+ # # Collage Maker DB (optional)
70
+ # COLLAGE_MAKER_DB_URL = os.getenv("COLLAGE_MAKER_DB_URL")
71
+ # collage_maker_client = None
72
+ # collage_maker_db = None
73
+ # collage_media_clicks_col = None
74
+ # if COLLAGE_MAKER_DB_URL:
75
+ # try:
76
+ # collage_maker_client = AsyncIOMotorClient(COLLAGE_MAKER_DB_URL)
77
+ # collage_maker_db = collage_maker_client.adminPanel
78
+ # collage_media_clicks_col = collage_maker_db.media_clicks
79
+ # except Exception as e:
80
+ # logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
81
+
82
+ # # AI Enhancer DB (optional)
83
+
84
+ # AI_ENHANCER_DB_URL = os.getenv("AI_ENHANCER_DB_URL")
85
+ # ai_enhancer_client = None
86
+ # ai_enhancer_db = None
87
+ # ai_enhancer_media_clicks_col = None
88
+ # ai_enhancer_subcategories_col = None
89
+
90
+ # if AI_ENHANCER_DB_URL:
91
+ # try:
92
+ # ai_enhancer_client = AsyncIOMotorClient(AI_ENHANCER_DB_URL)
93
+ # ai_enhancer_db = ai_enhancer_client.test # 🔴 test database
94
+ # ai_enhancer_media_clicks_col = ai_enhancer_db.media_clicks
95
+ # ai_enhancer_subcategories_col = ai_enhancer_db.subcategories
96
+ # except Exception as e:
97
+ # logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
98
+
99
+ # # OLD logs DB
100
+ # MONGODB_URL = os.getenv("MONGODB_URL")
101
+ # client = None
102
+ # database = None
103
+
104
+ # # --------------------- Download Models ---------------------
105
+ # def download_models():
106
+ # try:
107
+ # logger.info("Downloading models...")
108
+ # inswapper_path = hf_hub_download(
109
+ # repo_id=REPO_ID,
110
+ # filename="models/inswapper_128.onnx",
111
+ # repo_type="model",
112
+ # local_dir=MODELS_DIR,
113
+ # token=HF_TOKEN
114
+ # )
115
+
116
+ # buffalo_files = ["1k3d68.onnx", "2d106det.onnx", "genderage.onnx", "det_10g.onnx", "w600k_r50.onnx"]
117
+ # for f in buffalo_files:
118
+ # hf_hub_download(
119
+ # repo_id=REPO_ID,
120
+ # filename=f"models/buffalo_l/" + f,
121
+ # repo_type="model",
122
+ # local_dir=MODELS_DIR,
123
+ # token=HF_TOKEN
124
+ # )
125
+
126
+ # logger.info("Models downloaded successfully.")
127
+ # return inswapper_path
128
+ # except Exception as e:
129
+ # logger.error(f"Model download failed: {e}")
130
+ # raise
131
+
132
+ # try:
133
+ # inswapper_path = download_models()
134
+
135
+ # # --------------------- Face Analysis + Swapper ---------------------
136
+ # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
137
+ # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
138
+ # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
139
+ # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
140
+ # logger.info("Face analysis models loaded successfully")
141
+ # except Exception as e:
142
+ # logger.error(f"Failed to initialize face analysis models: {e}")
143
+ # # Set defaults to prevent crash
144
+ # inswapper_path = None
145
+ # face_analysis_app = None
146
+ # swapper = None
147
+
148
+ # # --------------------- CodeFormer ---------------------
149
+ # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
150
+
151
+ # def ensure_codeformer():
152
+ # try:
153
+ # if not os.path.exists("CodeFormer"):
154
+ # logger.info("CodeFormer not found, cloning repository...")
155
+ # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
156
+ # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=False) # Non-critical deps
157
+
158
+ # # Always ensure BasicSR is installed from local directory
159
+ # # This is needed for Hugging Face Spaces where BasicSR can't be installed from GitHub
160
+ # if os.path.exists("CodeFormer/basicsr/setup.py"):
161
+ # logger.info("Installing BasicSR from local directory...")
162
+ # subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
163
+ # logger.info("BasicSR installed successfully")
164
+
165
+ # # Install realesrgan after BasicSR is installed (realesrgan depends on BasicSR)
166
+ # # This must be done after BasicSR installation to avoid PyPI install issues
167
+ # try:
168
+ # import realesrgan
169
+ # logger.info("RealESRGAN already installed")
170
+ # except ImportError:
171
+ # logger.info("Installing RealESRGAN...")
172
+ # subprocess.run("pip install --no-cache-dir realesrgan", shell=True, check=True)
173
+ # logger.info("RealESRGAN installed successfully")
174
+
175
+ # # Download models if CodeFormer exists (fixed logic)
176
+ # if os.path.exists("CodeFormer"):
177
+ # try:
178
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=False, timeout=300)
179
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
180
+ # logger.warning("Failed to download facelib models (optional)")
181
+ # try:
182
+ # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=False, timeout=300)
183
+ # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
184
+ # logger.warning("Failed to download CodeFormer models (optional)")
185
+ # except Exception as e:
186
+ # logger.error(f"CodeFormer setup failed: {e}")
187
+ # logger.warning("Continuing without CodeFormer features...")
188
+
189
+ # ensure_codeformer()
190
+ # # --------------------- FastAPI ---------------------
191
+ # fastapi_app = FastAPI()
192
+
193
+ # @fastapi_app.on_event("startup")
194
+ # async def startup_db():
195
+ # global client, database
196
+ # if MONGODB_URL:
197
+ # try:
198
+ # logger.info("Initializing MongoDB for API logs...")
199
+ # client = AsyncIOMotorClient(MONGODB_URL)
200
+ # database = client.FaceSwap
201
+ # logger.info("MongoDB initialized for API logs")
202
+ # except Exception as e:
203
+ # logger.warning(f"MongoDB connection failed (optional): {e}")
204
+ # client = None
205
+ # database = None
206
+ # else:
207
+ # logger.warning("MONGODB_URL not set, skipping MongoDB initialization")
208
+
209
+ # @fastapi_app.on_event("shutdown")
210
+ # async def shutdown_db():
211
+ # global client, admin_client, collage_maker_client
212
+ # if client is not None:
213
+ # client.close()
214
+ # logger.info("MongoDB connection closed")
215
+ # if admin_client is not None:
216
+ # admin_client.close()
217
+ # logger.info("Admin MongoDB connection closed")
218
+ # if collage_maker_client is not None:
219
+ # collage_maker_client.close()
220
+ # logger.info("Collage Maker MongoDB connection closed")
221
+
222
+ # # --------------------- Auth ---------------------
223
+ # security = HTTPBearer()
224
+
225
+ # def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
226
+ # if credentials.credentials != API_SECRET_TOKEN:
227
+ # raise HTTPException(status_code=401, detail="Invalid or missing token")
228
+ # return credentials.credentials
229
+
230
+ # # --------------------- DB Selector ---------------------
231
+ # # def get_media_clicks_collection(appname: Optional[str] = None):
232
+ # # """
233
+ # # Returns the correct media_clicks collection based on appname.
234
+ # # Defaults to the primary admin database when no appname is provided
235
+ # # or when the requested database is unavailable.
236
+ # # """
237
+ # # if appname:
238
+ # # normalized = appname.strip().lower()
239
+ # # if normalized == "collage-maker":
240
+ # # if collage_media_clicks_col is not None:
241
+ # # return collage_media_clicks_col
242
+ # # logger.warning("COLLAGE_MAKER_DB_URL not configured; falling back to default media_clicks collection")
243
+ # # return media_clicks_col
244
+ # def get_app_db_collections(appname: Optional[str] = None):
245
+ # """
246
+ # Returns (media_clicks_collection, subcategories_collection)
247
+ # based on appname.
248
+ # """
249
+
250
+ # if appname:
251
+ # app = appname.strip().lower()
252
+
253
+ # if app == "collage-maker":
254
+ # if collage_media_clicks_col is not None and subcategories_col is not None:
255
+ # return collage_media_clicks_col, subcategories_col
256
+ # logger.warning("Collage-maker DB not configured, falling back to admin")
257
+
258
+ # elif app == "ai-enhancer":
259
+ # if ai_enhancer_media_clicks_col is not None and ai_enhancer_subcategories_col is not None:
260
+ # return ai_enhancer_media_clicks_col, ai_enhancer_subcategories_col
261
+ # logger.warning("AI-Enhancer DB not configured, falling back to admin")
262
+
263
+ # # default fallback
264
+ # return media_clicks_col, subcategories_col
265
+
266
+
267
+
268
+ # # --------------------- Logging API Hits ---------------------
269
+ # async def log_faceswap_hit(token: str, status: str = "success"):
270
+ # global database
271
+ # if database is None:
272
+ # return
273
+ # await database.api_logs.insert_one({
274
+ # "token": token,
275
+ # "endpoint": "/faceswap",
276
+ # "status": status,
277
+ # "timestamp": datetime.utcnow()
278
+ # })
279
+
280
+ # # --------------------- Face Swap Pipeline ---------------------
281
+ # swap_lock = threading.Lock()
282
+
283
+ # def enhance_image_with_codeformer(rgb_img, temp_dir=None):
284
+ # if temp_dir is None:
285
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"enhance_{uuid.uuid4().hex[:8]}")
286
+ # os.makedirs(temp_dir, exist_ok=True)
287
+
288
+ # input_path = os.path.join(temp_dir, "input.jpg")
289
+ # cv2.imwrite(input_path, cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR))
290
+
291
+ # python_cmd = sys.executable if sys.executable else "python3"
292
+ # cmd = (
293
+ # f"{python_cmd} {CODEFORMER_PATH} "
294
+ # f"-w 0.7 "
295
+ # f"--input_path {input_path} "
296
+ # f"--output_path {temp_dir} "
297
+ # f"--bg_upsampler realesrgan "
298
+ # f"--face_upsample"
299
+ # )
300
+
301
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
302
+ # if result.returncode != 0:
303
+ # raise RuntimeError(result.stderr)
304
+
305
+ # final_dir = os.path.join(temp_dir, "final_results")
306
+ # files = [f for f in os.listdir(final_dir) if f.endswith(".png")]
307
+ # if not files:
308
+ # raise RuntimeError("No enhanced output")
309
+
310
+ # final_path = os.path.join(final_dir, files[0])
311
+ # enhanced = cv2.imread(final_path)
312
+ # return cv2.cvtColor(enhanced, cv2.COLOR_BGR2RGB)
313
+
314
+ # def multi_face_swap(src_img, tgt_img):
315
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
316
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
317
+
318
+ # src_faces = face_analysis_app.get(src_bgr)
319
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
320
+
321
+ # if not src_faces or not tgt_faces:
322
+ # raise ValueError("No faces detected")
323
+
324
+ # def face_sort_key(face):
325
+ # x1, y1, x2, y2 = face.bbox
326
+ # area = (x2 - x1) * (y2 - y1)
327
+ # cx = (x1 + x2) / 2
328
+ # return (-area, cx)
329
+
330
+ # # Split by gender
331
+ # src_male = [f for f in src_faces if f.gender == 1]
332
+ # src_female = [f for f in src_faces if f.gender == 0]
333
+
334
+ # tgt_male = [f for f in tgt_faces if f.gender == 1]
335
+ # tgt_female = [f for f in tgt_faces if f.gender == 0]
336
+
337
+ # # Sort inside gender groups
338
+ # src_male = sorted(src_male, key=face_sort_key)
339
+ # src_female = sorted(src_female, key=face_sort_key)
340
+
341
+ # tgt_male = sorted(tgt_male, key=face_sort_key)
342
+ # tgt_female = sorted(tgt_female, key=face_sort_key)
343
+
344
+ # # Build final swap pairs
345
+ # pairs = []
346
+
347
+ # for s, t in zip(src_male, tgt_male):
348
+ # pairs.append((s, t))
349
+
350
+ # for s, t in zip(src_female, tgt_female):
351
+ # pairs.append((s, t))
352
+
353
+ # # Fallback if gender mismatch
354
+ # if not pairs:
355
+ # src_faces = sorted(src_faces, key=face_sort_key)
356
+ # tgt_faces = sorted(tgt_faces, key=face_sort_key)
357
+ # pairs = list(zip(src_faces, tgt_faces))
358
+
359
+ # result_img = tgt_bgr.copy()
360
+
361
+ # for src_face, _ in pairs:
362
+ # # 🔁 re-detect current target faces
363
+ # if face_analysis_app is None:
364
+ # raise ValueError("Face analysis models not initialized. Please ensure models are downloaded.")
365
+ # current_faces = face_analysis_app.get(result_img)
366
+ # current_faces = sorted(current_faces, key=face_sort_key)
367
+
368
+ # # choose best matching gender
369
+ # candidates = [
370
+ # f for f in current_faces if f.gender == src_face.gender
371
+ # ] or current_faces
372
+
373
+ # target_face = candidates[0]
374
+
375
+ # if swapper is None:
376
+ # raise ValueError("Face swap models not initialized. Please ensure models are downloaded.")
377
+ # result_img = swapper.get(
378
+ # result_img,
379
+ # target_face,
380
+ # src_face,
381
+ # paste_back=True
382
+ # )
383
+
384
+ # return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
385
+
386
+
387
+
388
+ # def face_swap_and_enhance(src_img, tgt_img, temp_dir=None):
389
+ # try:
390
+ # with swap_lock:
391
+ # # Use a temp dir for intermediate files
392
+ # if temp_dir is None:
393
+ # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_work_{uuid.uuid4().hex[:8]}")
394
+ # if os.path.exists(temp_dir):
395
+ # shutil.rmtree(temp_dir)
396
+ # os.makedirs(temp_dir, exist_ok=True)
397
+
398
+ # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
399
+ # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
400
+
401
+ # src_faces = face_analysis_app.get(src_bgr)
402
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
403
+ # if face_analysis_app is None:
404
+ # return None, None, "❌ Face analysis models not initialized. Please ensure models are downloaded."
405
+ # if not src_faces or not tgt_faces:
406
+ # return None, None, "❌ Face not detected in one of the images"
407
+
408
+ # swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
409
+ # if swapper is None:
410
+ # return None, None, "❌ Face swap models not initialized. Please ensure models are downloaded."
411
+ # swapped_bgr = swapper.get(tgt_bgr, tgt_faces[0], src_faces[0])
412
+ # if swapped_bgr is None:
413
+ # return None, None, "❌ Face swap failed"
414
+
415
+ # cv2.imwrite(swapped_path, swapped_bgr)
416
+
417
+ # python_cmd = sys.executable if sys.executable else "python3"
418
+ # cmd = f"{python_cmd} {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
419
+ # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
420
+ # if result.returncode != 0:
421
+ # return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
422
+
423
+ # final_results_dir = os.path.join(temp_dir, "final_results")
424
+ # final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
425
+ # if not final_files:
426
+ # return None, None, "❌ No enhanced image found"
427
+
428
+ # final_path = os.path.join(final_results_dir, final_files[0])
429
+ # final_img_bgr = cv2.imread(final_path)
430
+ # if final_img_bgr is None:
431
+ # return None, None, "❌ Failed to read enhanced image file"
432
+ # final_img = cv2.cvtColor(final_img_bgr, cv2.COLOR_BGR2RGB)
433
+
434
+ # return final_img, final_path, ""
435
+
436
+ # except Exception as e:
437
+ # return None, None, f"❌ Error: {str(e)}"
438
+
439
+ # def compress_image(
440
+ # image_bytes: bytes,
441
+ # max_size=(1280, 1280), # max width/height
442
+ # quality=75 # JPEG quality (60–80 is ideal)
443
+ # ) -> bytes:
444
+ # """
445
+ # Compress image by resizing and lowering quality.
446
+ # Returns compressed image bytes.
447
+ # """
448
+ # img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
449
+
450
+ # # Resize while maintaining aspect ratio
451
+ # img.thumbnail(max_size, Image.LANCZOS)
452
+
453
+ # output = io.BytesIO()
454
+ # img.save(
455
+ # output,
456
+ # format="JPEG",
457
+ # quality=quality,
458
+ # optimize=True,
459
+ # progressive=True
460
+ # )
461
+
462
+ # return output.getvalue()
463
+
464
+ # # --------------------- DigitalOcean Spaces Helper ---------------------
465
+ # def get_spaces_client():
466
+ # session = boto3.session.Session()
467
+ # client = session.client(
468
+ # 's3',
469
+ # region_name=DO_SPACES_REGION,
470
+ # endpoint_url=DO_SPACES_ENDPOINT,
471
+ # aws_access_key_id=DO_SPACES_KEY,
472
+ # aws_secret_access_key=DO_SPACES_SECRET,
473
+ # config=Config(signature_version='s3v4')
474
+ # )
475
+ # return client
476
+
477
+ # def upload_to_spaces(file_bytes, key, content_type="image/png"):
478
+ # client = get_spaces_client()
479
+ # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
480
+ # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
481
+
482
+ # def download_from_spaces(key):
483
+ # client = get_spaces_client()
484
+ # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
485
+ # return obj['Body'].read()
486
+
487
+ # def build_multi_faceswap_gradio():
488
+ # with gr.Blocks() as demo:
489
+ # gr.Markdown("## 👩‍❤️‍👨 Multi Face Swap (Couple → Couple)")
490
+
491
+ # with gr.Row():
492
+ # src = gr.Image(type="numpy", label="Source Image (2 Faces)")
493
+ # tgt = gr.Image(type="numpy", label="Target Image (2 Faces)")
494
+
495
+ # out = gr.Image(type="numpy", label="Swapped Result")
496
+ # error = gr.Textbox(label="Logs", interactive=False)
497
+
498
+ # def process(src_img, tgt_img):
499
+ # try:
500
+ # swapped = multi_face_swap(src_img, tgt_img)
501
+ # enhanced = enhance_image_with_codeformer(swapped)
502
+ # return enhanced, ""
503
+ # except Exception as e:
504
+ # return None, str(e)
505
+
506
+ # btn = gr.Button("Swap Faces")
507
+ # btn.click(process, [src, tgt], [out, error])
508
+
509
+ # return demo
510
+
511
+ # def mandatory_enhancement(rgb_img):
512
+ # """
513
+ # Always runs CodeFormer on the final image.
514
+ # Fail-safe: returns original if enhancement fails.
515
+ # """
516
+ # try:
517
+ # return enhance_image_with_codeformer(rgb_img)
518
+ # except Exception as e:
519
+ # logger.error(f"CodeFormer failed, returning original: {e}")
520
+ # return rgb_img
521
+
522
+ # # --------------------- API Endpoints ---------------------
523
+ # @fastapi_app.get("/")
524
+ # async def root():
525
+ # """Root endpoint"""
526
+ # return {
527
+ # "success": True,
528
+ # "message": "FaceSwap API",
529
+ # "data": {
530
+ # "version": "1.0.0",
531
+ # "Product Name":"Beauty Camera - GlowCam AI Studio",
532
+ # "Released By" : "LogicGo Infotech"
533
+ # }
534
+ # }
535
+ # @fastapi_app.get("/health")
536
+ # async def health():
537
+ # return {"status": "healthy"}
538
+
539
+ # from fastapi import Form
540
+ # import requests
541
+ # @fastapi_app.get("/test-admin-db")
542
+ # async def test_admin_db():
543
+ # try:
544
+ # doc = await admin_db.list_collection_names()
545
+ # return {"ok": True, "collections": doc}
546
+ # except Exception as e:
547
+ # return {"ok": False, "error": str(e), "url": ADMIN_MONGO_URL}
548
+
549
+ # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
550
+ # async def face_swap_api(
551
+ # source: UploadFile = File(...),
552
+ # target_category_id: str = Form(None),
553
+ # new_category_id: str = Form(None),
554
+ # user_id: Optional[str] = Form(None),
555
+ # appname: Optional[str] = Form(None),
556
+ # credentials: HTTPAuthorizationCredentials = Security(security)
557
+ # ):
558
+ # start_time = datetime.utcnow()
559
+
560
+ # try:
561
+ # # ------------------------------------------------------------------
562
+ # # VALIDATION
563
+ # # ------------------------------------------------------------------
564
+ # # --------------------------------------------------------------
565
+ # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
566
+ # # --------------------------------------------------------------
567
+ # if target_category_id == "":
568
+ # target_category_id = None
569
+
570
+ # if new_category_id == "":
571
+ # new_category_id = None
572
+
573
+ # if user_id == "":
574
+ # user_id = None
575
+
576
+ # # media_clicks_collection = get_media_clicks_collection(appname)
577
+ # media_clicks_collection, subcategories_collection = get_app_db_collections(appname)
578
+
579
+
580
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
581
+
582
+ # if target_category_id and new_category_id:
583
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
584
+
585
+ # if not target_category_id and not new_category_id:
586
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
587
+
588
+ # # ------------------------------------------------------------------
589
+ # # READ SOURCE IMAGE
590
+ # # ------------------------------------------------------------------
591
+ # src_bytes = await source.read()
592
+ # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
593
+ # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
594
+
595
+ # # ------------------------------------------------------------------
596
+ # # CASE 1 : new_category_id → MongoDB lookup
597
+ # # ------------------------------------------------------------------
598
+ # if new_category_id:
599
+
600
+ # # doc = await subcategories_col.find_one({
601
+ # # "asset_images._id": ObjectId(new_category_id)
602
+ # # })
603
+ # doc = await subcategories_collection.find_one({
604
+ # "asset_images._id": ObjectId(new_category_id)
605
+ # })
606
+
607
+
608
+ # if not doc:
609
+ # raise HTTPException(404, "Asset image not found in database")
610
+
611
+ # # extract correct asset
612
+ # asset = next(
613
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
614
+ # None
615
+ # )
616
+
617
+ # if not asset:
618
+ # raise HTTPException(404, "Asset image URL not found")
619
+
620
+ # # correct URL
621
+ # target_url = asset["url"]
622
+
623
+ # # correct categoryId (ObjectId)
624
+ # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
625
+ # subcategory_oid = doc["_id"]
626
+
627
+ # # ------------------------------------------------------------------#
628
+ # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
629
+ # # ------------------------------------------------------------------#
630
+ # if user_id and media_clicks_collection is not None:
631
+ # try:
632
+ # user_id_clean = user_id.strip()
633
+ # if not user_id_clean:
634
+ # raise ValueError("user_id cannot be empty")
635
+ # try:
636
+ # user_oid = ObjectId(user_id_clean)
637
+ # except (InvalidId, ValueError) as e:
638
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
639
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
640
+
641
+ # now = datetime.utcnow()
642
+
643
+ # # Normalize dates (UTC midnight)
644
+ # today_date = datetime(now.year, now.month, now.day)
645
+
646
+ # # -------------------------------------------------
647
+ # # STEP 1: Ensure root document exists
648
+ # # -------------------------------------------------
649
+ # await media_clicks_collection.update_one(
650
+ # {"userId": user_oid},
651
+ # {
652
+ # "$setOnInsert": {
653
+ # "userId": user_oid,
654
+ # "createdAt": now,
655
+ # "ai_edit_complete": 0,
656
+ # "ai_edit_daily_count": []
657
+ # }
658
+ # },
659
+ # upsert=True
660
+ # )
661
+ # # -------------------------------------------------
662
+ # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
663
+ # # -------------------------------------------------
664
+ # doc = await media_clicks_collection.find_one(
665
+ # {"userId": user_oid},
666
+ # {"ai_edit_daily_count": 1}
667
+ # )
668
+
669
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
670
+
671
+ # # Normalize today to UTC midnight
672
+ # today_date = datetime(now.year, now.month, now.day)
673
+
674
+ # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
675
+ # daily_map = {}
676
+ # for entry in daily_entries:
677
+ # d = entry["date"]
678
+ # if isinstance(d, datetime):
679
+ # d = datetime(d.year, d.month, d.day)
680
+ # daily_map[d] = entry["count"] # overwrite = no duplicates
681
+
682
+ # # Determine last recorded date
683
+ # last_date = max(daily_map.keys()) if daily_map else today_date
684
+
685
+ # # Fill ALL missing days with count = 0
686
+ # next_day = last_date + timedelta(days=1)
687
+ # while next_day < today_date:
688
+ # daily_map.setdefault(next_day, 0)
689
+ # next_day += timedelta(days=1)
690
+
691
+ # # Mark today as used (binary)
692
+ # daily_map[today_date] = 1
693
+
694
+ # # Rebuild list: OLDEST → NEWEST
695
+ # final_daily_entries = [
696
+ # {"date": d, "count": daily_map[d]}
697
+ # for d in sorted(daily_map.keys())
698
+ # ]
699
+
700
+ # # Keep only last 32 days
701
+ # final_daily_entries = final_daily_entries[-32:]
702
+
703
+ # # Atomic replace
704
+ # await media_clicks_collection.update_one(
705
+ # {"userId": user_oid},
706
+ # {
707
+ # "$set": {
708
+ # "ai_edit_daily_count": final_daily_entries,
709
+ # "updatedAt": now
710
+ # }
711
+ # }
712
+ # )
713
+
714
+ # # -------------------------------------------------
715
+ # # STEP 3: Try updating existing subCategory
716
+ # # -------------------------------------------------
717
+ # update_result = await media_clicks_collection.update_one(
718
+ # {
719
+ # "userId": user_oid,
720
+ # "subCategories.subCategoryId": subcategory_oid
721
+ # },
722
+ # {
723
+ # "$inc": {
724
+ # "subCategories.$.click_count": 1,
725
+ # "ai_edit_complete": 1
726
+ # },
727
+ # "$set": {
728
+ # "subCategories.$.lastClickedAt": now,
729
+ # "ai_edit_last_date": now,
730
+ # "updatedAt": now
731
+ # }
732
+ # }
733
+ # )
734
+
735
+ # # -------------------------------------------------
736
+ # # STEP 4: Push subCategory if missing
737
+ # # -------------------------------------------------
738
+ # if update_result.matched_count == 0:
739
+ # await media_clicks_collection.update_one(
740
+ # {"userId": user_oid},
741
+ # {
742
+ # "$inc": {
743
+ # "ai_edit_complete": 1
744
+ # },
745
+ # "$set": {
746
+ # "ai_edit_last_date": now,
747
+ # "updatedAt": now
748
+ # },
749
+ # "$push": {
750
+ # "subCategories": {
751
+ # "subCategoryId": subcategory_oid,
752
+ # "click_count": 1,
753
+ # "lastClickedAt": now
754
+ # }
755
+ # }
756
+ # }
757
+ # )
758
+
759
+ # # -------------------------------------------------
760
+ # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
761
+ # # -------------------------------------------------
762
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
763
+ # if user_doc and "subCategories" in user_doc:
764
+ # subcategories = user_doc["subCategories"]
765
+ # # Sort by lastClickedAt in ascending order (oldest first)
766
+ # # Handle missing or None dates by using datetime.min
767
+ # subcategories_sorted = sorted(
768
+ # subcategories,
769
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
770
+ # )
771
+ # # Update with sorted array
772
+ # await media_clicks_collection.update_one(
773
+ # {"userId": user_oid},
774
+ # {
775
+ # "$set": {
776
+ # "subCategories": subcategories_sorted,
777
+ # "updatedAt": now
778
+ # }
779
+ # }
780
+ # )
781
+
782
+ # logger.info(
783
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
784
+ # user_id,
785
+ # str(subcategory_oid)
786
+ # )
787
+
788
+ # except Exception as media_err:
789
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
790
+ # elif user_id and media_clicks_collection is None:
791
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
792
+
793
+ # # # ------------------------------------------------------------------
794
+ # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
795
+ # # # ------------------------------------------------------------------
796
+ # if target_category_id:
797
+ # client = get_spaces_client()
798
+ # base_prefix = "faceswap/target/"
799
+ # resp = client.list_objects_v2(
800
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
801
+ # )
802
+
803
+ # # Extract categories from the CommonPrefixes
804
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
805
+
806
+ # target_url = None
807
+
808
+ # # --- FIX STARTS HERE ---
809
+ # for category in categories:
810
+ # original_prefix = f"faceswap/target/{category}/original/"
811
+ # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
812
+
813
+ # # List objects in original/
814
+ # original_objects = client.list_objects_v2(
815
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
816
+ # ).get("Contents", [])
817
+
818
+ # # List objects in thumb/ (optional: for the old code's extra check)
819
+ # thumb_objects = client.list_objects_v2(
820
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
821
+ # ).get("Contents", [])
822
+
823
+ # # Extract only the filenames and filter for .png
824
+ # original_filenames = sorted([
825
+ # obj["Key"].split("/")[-1] for obj in original_objects
826
+ # if obj["Key"].split("/")[-1].endswith(".png")
827
+ # ])
828
+ # thumb_filenames = [
829
+ # obj["Key"].split("/")[-1] for obj in thumb_objects
830
+ # ]
831
+
832
+ # # Replicate the old indexing logic based on sorted filenames
833
+ # for idx, filename in enumerate(original_filenames, start=1):
834
+ # cid = f"{category.lower()}image_{idx}"
835
+
836
+ # # Optional: Replicate the thumb file check for 100% parity
837
+ # # if filename in thumb_filenames and cid == target_category_id:
838
+ # # Simpler check just on the ID, assuming thumb files are present
839
+ # if cid == target_category_id:
840
+ # # Construct the final target URL using the full prefix and the filename
841
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
842
+ # break
843
+
844
+ # if target_url:
845
+ # break
846
+ # # --- FIX ENDS HERE ---
847
+
848
+ # if not target_url:
849
+ # raise HTTPException(404, "Target categoryId not found")
850
+ # # # ------------------------------------------------------------------
851
+ # # # DOWNLOAD TARGET IMAGE
852
+ # # # ------------------------------------------------------------------
853
+ # async with httpx.AsyncClient(timeout=30.0) as client:
854
+ # response = await client.get(target_url)
855
+ # response.raise_for_status()
856
+ # tgt_bytes = response.content
857
+
858
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
859
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
860
+
861
+ # if src_bgr is None or tgt_bgr is None:
862
+ # raise HTTPException(400, "Invalid image data")
863
+
864
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
865
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
866
+
867
+ # # ------------------------------------------------------------------
868
+ # # FACE SWAP EXECUTION
869
+ # # ------------------------------------------------------------------
870
+ # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
871
+
872
+ # # #--------------------Version 2.0 ----------------------------------------#
873
+ # # final_img, final_path, err = enhanced_face_swap_and_enhance(src_rgb, tgt_rgb)
874
+ # # #--------------------Version 2.0 ----------------------------------------#
875
+
876
+ # if err:
877
+ # raise HTTPException(500, err)
878
+
879
+ # with open(final_path, "rb") as f:
880
+ # result_bytes = f.read()
881
+
882
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
883
+ # result_url = upload_to_spaces(result_bytes, result_key)
884
+ # # -------------------------------------------------
885
+ # # COMPRESS IMAGE (2–3 MB target)
886
+ # # -------------------------------------------------
887
+ # compressed_bytes = compress_image(
888
+ # image_bytes=result_bytes,
889
+ # max_size=(1280, 1280),
890
+ # quality=72
891
+ # )
892
+
893
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
894
+ # compressed_url = upload_to_spaces(
895
+ # compressed_bytes,
896
+ # compressed_key,
897
+ # content_type="image/jpeg"
898
+ # )
899
+ # end_time = datetime.utcnow()
900
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
901
+
902
+ # if database is not None:
903
+ # log_entry = {
904
+ # "endpoint": "/face-swap",
905
+ # "status": "success",
906
+ # "response_time_ms": response_time_ms,
907
+ # "timestamp": end_time
908
+ # }
909
+ # if appname:
910
+ # log_entry["appname"] = appname
911
+ # await database.api_logs.insert_one(log_entry)
912
+
913
+
914
+ # return {
915
+ # "result_key": result_key,
916
+ # "result_url": result_url,
917
+ # "Compressed_Image_URL": compressed_url
918
+ # }
919
+
920
+ # except Exception as e:
921
+ # end_time = datetime.utcnow()
922
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
923
+
924
+ # if database is not None:
925
+ # log_entry = {
926
+ # "endpoint": "/face-swap",
927
+ # "status": "fail",
928
+ # "response_time_ms": response_time_ms,
929
+ # "timestamp": end_time,
930
+ # "error": str(e)
931
+ # }
932
+ # if appname:
933
+ # log_entry["appname"] = appname
934
+ # await database.api_logs.insert_one(log_entry)
935
+
936
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
937
+
938
+ # @fastapi_app.get("/preview/{result_key:path}")
939
+ # async def preview_result(result_key: str):
940
+ # try:
941
+ # img_bytes = download_from_spaces(result_key)
942
+ # except Exception:
943
+ # raise HTTPException(status_code=404, detail="Result not found")
944
+ # return Response(
945
+ # content=img_bytes,
946
+ # media_type="image/png",
947
+ # headers={"Content-Disposition": "inline; filename=result.png"}
948
+ # )
949
+
950
+ # @fastapi_app.post("/multi-face-swap", dependencies=[Depends(verify_token)])
951
+ # async def multi_face_swap_api(
952
+ # source_image: UploadFile = File(...),
953
+ # target_image: UploadFile = File(...)
954
+ # ):
955
+ # start_time = datetime.utcnow()
956
+
957
+ # try:
958
+ # # -----------------------------
959
+ # # Read images
960
+ # # -----------------------------
961
+ # src_bytes = await source_image.read()
962
+ # tgt_bytes = await target_image.read()
963
+
964
+ # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
965
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
966
+
967
+ # if src_bgr is None or tgt_bgr is None:
968
+ # raise HTTPException(400, "Invalid image data")
969
+
970
+ # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
971
+ # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
972
+
973
+ # # -----------------------------
974
+ # # Multi-face swap
975
+ # # -----------------------------
976
+ # swapped_rgb = multi_face_swap(src_rgb, tgt_rgb)
977
+
978
+ # # -----------------------------
979
+ # # 🔥 MANDATORY ENHANCEMENT
980
+ # # -----------------------------
981
+ # final_rgb = mandatory_enhancement(swapped_rgb)
982
+
983
+ # final_bgr = cv2.cvtColor(final_rgb, cv2.COLOR_RGB2BGR)
984
+
985
+ # # -----------------------------
986
+ # # Save temp result
987
+ # # -----------------------------
988
+ # temp_dir = tempfile.mkdtemp(prefix="multi_faceswap_")
989
+ # result_path = os.path.join(temp_dir, "result.png")
990
+ # cv2.imwrite(result_path, final_bgr)
991
+
992
+ # with open(result_path, "rb") as f:
993
+ # result_bytes = f.read()
994
+
995
+ # # -----------------------------
996
+ # # Upload
997
+ # # -----------------------------
998
+ # result_key = f"faceswap/multi/{uuid.uuid4().hex}.png"
999
+ # result_url = upload_to_spaces(
1000
+ # result_bytes,
1001
+ # result_key,
1002
+ # content_type="image/png"
1003
+ # )
1004
+
1005
+ # return {
1006
+ # "result_key": result_key,
1007
+ # "result_url": result_url
1008
+ # }
1009
+
1010
+ # except Exception as e:
1011
+ # raise HTTPException(status_code=500, detail=str(e))
1012
+
1013
+
1014
+ # @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
1015
+ # async def face_swap_api(
1016
+ # image1: UploadFile = File(...),
1017
+ # image2: Optional[UploadFile] = File(None),
1018
+ # target_category_id: str = Form(None),
1019
+ # new_category_id: str = Form(None),
1020
+ # user_id: Optional[str] = Form(None),
1021
+ # appname: Optional[str] = Form(None),
1022
+ # credentials: HTTPAuthorizationCredentials = Security(security)
1023
+ # ):
1024
+ # """
1025
+ # Production-ready face swap endpoint supporting:
1026
+ # - Multiple source images (image1 + optional image2)
1027
+ # - Gender-based pairing
1028
+ # - Merged faces from multiple sources
1029
+ # - Mandatory CodeFormer enhancement
1030
+ # """
1031
+ # start_time = datetime.utcnow()
1032
+
1033
+ # try:
1034
+ # # -----------------------------
1035
+ # # Validate input
1036
+ # # -----------------------------
1037
+ # if target_category_id == "":
1038
+ # target_category_id = None
1039
+ # if new_category_id == "":
1040
+ # new_category_id = None
1041
+ # if user_id == "":
1042
+ # user_id = None
1043
+
1044
+ # media_clicks_collection = get_media_clicks_collection(appname)
1045
+
1046
+ # if target_category_id and new_category_id:
1047
+ # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
1048
+ # if not target_category_id and not new_category_id:
1049
+ # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
1050
+
1051
+ # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
1052
+
1053
+ # # -----------------------------
1054
+ # # Read source images
1055
+ # # -----------------------------
1056
+ # src_images = []
1057
+ # img1_bytes = await image1.read()
1058
+ # src1 = cv2.imdecode(np.frombuffer(img1_bytes, np.uint8), cv2.IMREAD_COLOR)
1059
+ # if src1 is None:
1060
+ # raise HTTPException(400, "Invalid image1 data")
1061
+ # src_images.append(cv2.cvtColor(src1, cv2.COLOR_BGR2RGB))
1062
+
1063
+ # if image2:
1064
+ # img2_bytes = await image2.read()
1065
+ # src2 = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
1066
+ # if src2 is not None:
1067
+ # src_images.append(cv2.cvtColor(src2, cv2.COLOR_BGR2RGB))
1068
+
1069
+ # # -----------------------------
1070
+ # # Resolve target image
1071
+ # # -----------------------------
1072
+ # target_url = None
1073
+ # if new_category_id:
1074
+ # doc = await subcategories_col.find_one({
1075
+ # "asset_images._id": ObjectId(new_category_id)
1076
+ # })
1077
+
1078
+ # if not doc:
1079
+ # raise HTTPException(404, "Asset image not found in database")
1080
+
1081
+ # asset = next(
1082
+ # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
1083
+ # None
1084
+ # )
1085
+
1086
+ # if not asset:
1087
+ # raise HTTPException(404, "Asset image URL not found")
1088
+
1089
+ # target_url = asset["url"]
1090
+ # subcategory_oid = doc["_id"]
1091
+
1092
+ # if user_id and media_clicks_collection is not None:
1093
+ # try:
1094
+ # user_id_clean = user_id.strip()
1095
+ # if not user_id_clean:
1096
+ # raise ValueError("user_id cannot be empty")
1097
+ # try:
1098
+ # user_oid = ObjectId(user_id_clean)
1099
+ # except (InvalidId, ValueError):
1100
+ # logger.error(f"Invalid user_id format: {user_id_clean}")
1101
+ # raise ValueError(f"Invalid user_id format: {user_id_clean}")
1102
+
1103
+ # now = datetime.utcnow()
1104
+
1105
+ # # Step 1: ensure root document exists
1106
+ # await media_clicks_collection.update_one(
1107
+ # {"userId": user_oid},
1108
+ # {
1109
+ # "$setOnInsert": {
1110
+ # "userId": user_oid,
1111
+ # "createdAt": now,
1112
+ # "ai_edit_complete": 0,
1113
+ # "ai_edit_daily_count": []
1114
+ # }
1115
+ # },
1116
+ # upsert=True
1117
+ # )
1118
+
1119
+ # # Step 2: handle daily usage (binary, no duplicates)
1120
+ # doc = await media_clicks_collection.find_one(
1121
+ # {"userId": user_oid},
1122
+ # {"ai_edit_daily_count": 1}
1123
+ # )
1124
+
1125
+ # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
1126
+
1127
+ # today_date = datetime(now.year, now.month, now.day)
1128
+
1129
+ # daily_map = {}
1130
+ # for entry in daily_entries:
1131
+ # d = entry["date"]
1132
+ # if isinstance(d, datetime):
1133
+ # d = datetime(d.year, d.month, d.day)
1134
+ # daily_map[d] = entry["count"]
1135
+
1136
+ # last_date = max(daily_map.keys()) if daily_map else None
1137
+
1138
+ # if last_date != today_date:
1139
+ # daily_map[today_date] = 1
1140
+
1141
+ # final_daily_entries = [
1142
+ # {"date": d, "count": daily_map[d]}
1143
+ # for d in sorted(daily_map.keys())
1144
+ # ]
1145
+
1146
+ # final_daily_entries = final_daily_entries[-32:]
1147
+
1148
+ # await media_clicks_collection.update_one(
1149
+ # {"userId": user_oid},
1150
+ # {
1151
+ # "$set": {
1152
+ # "ai_edit_daily_count": final_daily_entries,
1153
+ # "updatedAt": now
1154
+ # }
1155
+ # }
1156
+ # )
1157
+
1158
+ # # Step 3: try updating existing subCategory
1159
+ # update_result = await media_clicks_collection.update_one(
1160
+ # {
1161
+ # "userId": user_oid,
1162
+ # "subCategories.subCategoryId": subcategory_oid
1163
+ # },
1164
+ # {
1165
+ # "$inc": {
1166
+ # "subCategories.$.click_count": 1,
1167
+ # "ai_edit_complete": 1
1168
+ # },
1169
+ # "$set": {
1170
+ # "subCategories.$.lastClickedAt": now,
1171
+ # "ai_edit_last_date": now,
1172
+ # "updatedAt": now
1173
+ # }
1174
+ # }
1175
+ # )
1176
+
1177
+ # # Step 4: push subCategory if missing
1178
+ # if update_result.matched_count == 0:
1179
+ # await media_clicks_collection.update_one(
1180
+ # {"userId": user_oid},
1181
+ # {
1182
+ # "$inc": {
1183
+ # "ai_edit_complete": 1
1184
+ # },
1185
+ # "$set": {
1186
+ # "ai_edit_last_date": now,
1187
+ # "updatedAt": now
1188
+ # },
1189
+ # "$push": {
1190
+ # "subCategories": {
1191
+ # "subCategoryId": subcategory_oid,
1192
+ # "click_count": 1,
1193
+ # "lastClickedAt": now
1194
+ # }
1195
+ # }
1196
+ # }
1197
+ # )
1198
+
1199
+ # # Step 5: sort subCategories by lastClickedAt (ascending)
1200
+ # user_doc = await media_clicks_collection.find_one({"userId": user_oid})
1201
+ # if user_doc and "subCategories" in user_doc:
1202
+ # subcategories = user_doc["subCategories"]
1203
+ # subcategories_sorted = sorted(
1204
+ # subcategories,
1205
+ # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
1206
+ # )
1207
+ # await media_clicks_collection.update_one(
1208
+ # {"userId": user_oid},
1209
+ # {
1210
+ # "$set": {
1211
+ # "subCategories": subcategories_sorted,
1212
+ # "updatedAt": now
1213
+ # }
1214
+ # }
1215
+ # )
1216
+
1217
+ # logger.info(
1218
+ # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
1219
+ # user_id,
1220
+ # str(subcategory_oid)
1221
+ # )
1222
+
1223
+ # except Exception as media_err:
1224
+ # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
1225
+ # elif user_id and media_clicks_collection is None:
1226
+ # logger.warning("Media clicks collection unavailable; skipping media click tracking")
1227
+
1228
+ # if target_category_id:
1229
+ # client = get_spaces_client()
1230
+ # base_prefix = "faceswap/target/"
1231
+ # resp = client.list_objects_v2(
1232
+ # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
1233
+ # )
1234
+
1235
+ # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
1236
+
1237
+ # for category in categories:
1238
+ # original_prefix = f"faceswap/target/{category}/original/"
1239
+ # thumb_prefix = f"faceswap/target/{category}/thumb/"
1240
+
1241
+ # original_objects = client.list_objects_v2(
1242
+ # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
1243
+ # ).get("Contents", [])
1244
+
1245
+ # thumb_objects = client.list_objects_v2(
1246
+ # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
1247
+ # ).get("Contents", [])
1248
+
1249
+ # original_filenames = sorted([
1250
+ # obj["Key"].split("/")[-1] for obj in original_objects
1251
+ # if obj["Key"].split("/")[-1].endswith(".png")
1252
+ # ])
1253
+
1254
+ # for idx, filename in enumerate(original_filenames, start=1):
1255
+ # cid = f"{category.lower()}image_{idx}"
1256
+ # if cid == target_category_id:
1257
+ # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
1258
+ # break
1259
+
1260
+ # if target_url:
1261
+ # break
1262
+
1263
+ # if not target_url:
1264
+ # raise HTTPException(404, "Target categoryId not found")
1265
+
1266
+ # async with httpx.AsyncClient(timeout=30.0) as client:
1267
+ # response = await client.get(target_url)
1268
+ # response.raise_for_status()
1269
+ # tgt_bytes = response.content
1270
+
1271
+ # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
1272
+ # if tgt_bgr is None:
1273
+ # raise HTTPException(400, "Invalid target image data")
1274
+
1275
+ # # -----------------------------
1276
+ # # Merge all source faces
1277
+ # # -----------------------------
1278
+ # all_src_faces = []
1279
+ # for img in src_images:
1280
+ # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
1281
+ # all_src_faces.extend(faces)
1282
+
1283
+ # if not all_src_faces:
1284
+ # raise HTTPException(400, "No faces detected in source images")
1285
+
1286
+ # tgt_faces = face_analysis_app.get(tgt_bgr)
1287
+ # if not tgt_faces:
1288
+ # raise HTTPException(400, "No faces detected in target image")
1289
+
1290
+ # # -----------------------------
1291
+ # # Gender-based pairing
1292
+ # # -----------------------------
1293
+ # def face_sort_key(face):
1294
+ # x1, y1, x2, y2 = face.bbox
1295
+ # area = (x2 - x1) * (y2 - y1)
1296
+ # cx = (x1 + x2) / 2
1297
+ # return (-area, cx)
1298
+
1299
+ # # Separate by gender
1300
+ # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
1301
+ # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
1302
+ # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
1303
+ # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
1304
+
1305
+ # pairs = []
1306
+ # for s, t in zip(src_male, tgt_male):
1307
+ # pairs.append((s, t))
1308
+ # for s, t in zip(src_female, tgt_female):
1309
+ # pairs.append((s, t))
1310
+
1311
+ # # fallback if gender mismatch
1312
+ # if not pairs:
1313
+ # src_all = sorted(all_src_faces, key=face_sort_key)
1314
+ # tgt_all = sorted(tgt_faces, key=face_sort_key)
1315
+ # pairs = list(zip(src_all, tgt_all))
1316
+
1317
+ # # -----------------------------
1318
+ # # Perform face swap
1319
+ # # -----------------------------
1320
+ # with swap_lock:
1321
+ # result_img = tgt_bgr.copy()
1322
+ # for src_face, _ in pairs:
1323
+ # if face_analysis_app is None:
1324
+ # raise HTTPException(status_code=500, detail="Face analysis models not initialized. Please ensure models are downloaded.")
1325
+ # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
1326
+ # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
1327
+ # target_face = candidates[0]
1328
+ # if swapper is None:
1329
+ # raise HTTPException(status_code=500, detail="Face swap models not initialized. Please ensure models are downloaded.")
1330
+ # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
1331
+
1332
+ # result_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
1333
+
1334
+ # # -----------------------------
1335
+ # # Mandatory enhancement
1336
+ # # -----------------------------
1337
+ # enhanced_rgb = mandatory_enhancement(result_rgb)
1338
+ # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
1339
+
1340
+ # # -----------------------------
1341
+ # # Save, upload, compress
1342
+ # # -----------------------------
1343
+ # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
1344
+ # final_path = os.path.join(temp_dir, "result.png")
1345
+ # cv2.imwrite(final_path, enhanced_bgr)
1346
+
1347
+ # with open(final_path, "rb") as f:
1348
+ # result_bytes = f.read()
1349
+
1350
+ # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
1351
+ # result_url = upload_to_spaces(result_bytes, result_key)
1352
+
1353
+ # compressed_bytes = compress_image(result_bytes, max_size=(1280, 1280), quality=72)
1354
+ # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
1355
+ # compressed_url = upload_to_spaces(compressed_bytes, compressed_key, content_type="image/jpeg")
1356
+
1357
+ # # -----------------------------
1358
+ # # Log API usage
1359
+ # # -----------------------------
1360
+ # end_time = datetime.utcnow()
1361
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
1362
+ # if database is not None:
1363
+ # log_entry = {
1364
+ # "endpoint": "/face-swap-couple",
1365
+ # "status": "success",
1366
+ # "response_time_ms": response_time_ms,
1367
+ # "timestamp": end_time
1368
+ # }
1369
+ # if appname:
1370
+ # log_entry["appname"] = appname
1371
+ # await database.api_logs.insert_one(log_entry)
1372
+
1373
+ # return {
1374
+ # "result_key": result_key,
1375
+ # "result_url": result_url,
1376
+ # "compressed_url": compressed_url
1377
+ # }
1378
+
1379
+ # except Exception as e:
1380
+ # end_time = datetime.utcnow()
1381
+ # response_time_ms = (end_time - start_time).total_seconds() * 1000
1382
+ # if database is not None:
1383
+ # log_entry = {
1384
+ # "endpoint": "/face-swap-couple",
1385
+ # "status": "fail",
1386
+ # "response_time_ms": response_time_ms,
1387
+ # "timestamp": end_time,
1388
+ # "error": str(e)
1389
+ # }
1390
+ # if appname:
1391
+ # log_entry["appname"] = appname
1392
+ # await database.api_logs.insert_one(log_entry)
1393
+ # raise HTTPException(500, f"Face swap failed: {str(e)}")
1394
+
1395
+
1396
+
1397
+
1398
+ # # --------------------- Mount Gradio ---------------------
1399
+
1400
+ # multi_faceswap_app = build_multi_faceswap_gradio()
1401
+ # fastapi_app = mount_gradio_app(
1402
+ # fastapi_app,
1403
+ # multi_faceswap_app,
1404
+ # path="/gradio-couple-faceswap"
1405
+ # )
1406
+
1407
+
1408
+
1409
+ # if __name__ == "__main__":
1410
+ # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
1411
+
1412
+
1413
  # --------------------- List Images Endpoint ---------------------
1414
  import os
1415
  os.environ["OMP_NUM_THREADS"] = "1"
 
1508
  except Exception as e:
1509
  logger.warning(f"MongoDB ai-enhancer connection failed (optional): {e}")
1510
 
1511
+
1512
+ def get_media_clicks_collection(appname: Optional[str] = None):
1513
+ """Return the media clicks collection for the given app (default: main admin)."""
1514
+ if appname and str(appname).strip().lower() == "collage-maker":
1515
+ return collage_media_clicks_col
1516
+ return media_clicks_col
1517
+
1518
+
1519
  # OLD logs DB
1520
  MONGODB_URL = os.getenv("MONGODB_URL")
1521
  client = None