LogicGoInfotechSpaces commited on
Commit
94ab61c
·
verified ·
1 Parent(s): b234055

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -1735
app.py CHANGED
@@ -1,1725 +1,3 @@
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
- # # OLD logs DB
70
- # MONGODB_URL = os.getenv("MONGODB_URL")
71
- # client = None
72
- # database = None
73
-
74
- # # --------------------- Download Models ---------------------
75
- # def download_models():
76
- # try:
77
- # logger.info("Downloading models...")
78
- # inswapper_path = hf_hub_download(
79
- # repo_id=REPO_ID,
80
- # filename="models/inswapper_128.onnx",
81
- # repo_type="model",
82
- # local_dir=MODELS_DIR,
83
- # token=HF_TOKEN
84
- # )
85
-
86
- # buffalo_files = ["1k3d68.onnx", "2d106det.onnx", "genderage.onnx", "det_10g.onnx", "w600k_r50.onnx"]
87
- # for f in buffalo_files:
88
- # hf_hub_download(
89
- # repo_id=REPO_ID,
90
- # filename=f"models/buffalo_l/" + f,
91
- # repo_type="model",
92
- # local_dir=MODELS_DIR,
93
- # token=HF_TOKEN
94
- # )
95
-
96
- # logger.info("Models downloaded successfully.")
97
- # return inswapper_path
98
- # except Exception as e:
99
- # logger.error(f"Model download failed: {e}")
100
- # raise
101
-
102
- # try:
103
- # inswapper_path = download_models()
104
-
105
- # # --------------------- Face Analysis + Swapper ---------------------
106
- # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
107
- # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
108
- # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
109
- # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
110
- # logger.info("Face analysis models loaded successfully")
111
- # except Exception as e:
112
- # logger.error(f"Failed to initialize face analysis models: {e}")
113
- # # Set defaults to prevent crash
114
- # inswapper_path = None
115
- # face_analysis_app = None
116
- # swapper = None
117
-
118
- # # --------------------- CodeFormer ---------------------
119
- # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
120
-
121
- # def ensure_codeformer():
122
- # try:
123
- # if not os.path.exists("CodeFormer"):
124
- # logger.info("CodeFormer not found, cloning repository...")
125
- # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
126
- # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=False) # Non-critical deps
127
-
128
- # # Always ensure BasicSR is installed from local directory
129
- # # This is needed for Hugging Face Spaces where BasicSR can't be installed from GitHub
130
- # if os.path.exists("CodeFormer/basicsr/setup.py"):
131
- # logger.info("Installing BasicSR from local directory...")
132
- # subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
133
- # logger.info("BasicSR installed successfully")
134
-
135
- # # Install realesrgan after BasicSR is installed (realesrgan depends on BasicSR)
136
- # # This must be done after BasicSR installation to avoid PyPI install issues
137
- # try:
138
- # import realesrgan
139
- # logger.info("RealESRGAN already installed")
140
- # except ImportError:
141
- # logger.info("Installing RealESRGAN...")
142
- # subprocess.run("pip install --no-cache-dir realesrgan", shell=True, check=True)
143
- # logger.info("RealESRGAN installed successfully")
144
-
145
- # # Download models if CodeFormer exists (fixed logic)
146
- # if os.path.exists("CodeFormer"):
147
- # try:
148
- # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=False, timeout=300)
149
- # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
150
- # logger.warning("Failed to download facelib models (optional)")
151
- # try:
152
- # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=False, timeout=300)
153
- # except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
154
- # logger.warning("Failed to download CodeFormer models (optional)")
155
- # except Exception as e:
156
- # logger.error(f"CodeFormer setup failed: {e}")
157
- # logger.warning("Continuing without CodeFormer features...")
158
-
159
- # ensure_codeformer()
160
-
161
- # # class NaturalFaceSwapper:
162
- # # """Enhanced face swapping with natural blending techniques"""
163
-
164
- # # def __init__(self, swapper, face_app):
165
- # # self.swapper = swapper
166
- # # self.face_app = face_app
167
-
168
- # # def match_color_histogram(self, source, target, mask=None):
169
- # # """Match color histogram of source to target for better blending"""
170
- # # if mask is None:
171
- # # mask = np.ones(source.shape[:2], dtype=np.uint8) * 255
172
-
173
- # # result = source.copy()
174
- # # for i in range(3): # Process each channel
175
- # # source_channel = source[:, :, i]
176
- # # target_channel = target[:, :, i]
177
-
178
- # # # Only use masked regions
179
- # # source_masked = source_channel[mask > 0]
180
- # # target_masked = target_channel[mask > 0]
181
-
182
- # # if len(source_masked) > 0 and len(target_masked) > 0:
183
- # # # Match histograms
184
- # # matched = self._match_histogram_channel(
185
- # # source_channel, source_masked, target_masked
186
- # # )
187
- # # result[:, :, i] = matched
188
-
189
- # # return result
190
- # # def subtle_skin_smooth(img, strength=0.3, preserve_details=True):
191
- # # """
192
- # # Subtle bilateral filter for natural skin smoothing
193
-
194
- # # Args:
195
- # # img: Input image (BGR format)
196
- # # strength: Smoothing strength (0.1-0.5 recommended, default 0.3)
197
- # # preserve_details: If True, uses edge-preserving filter
198
-
199
- # # Returns:
200
- # # Smoothed image
201
- # # """
202
- # # if preserve_details:
203
- # # # Bilateral filter preserves edges while smoothing
204
- # # smoothed = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)
205
- # # else:
206
- # # # Gaussian blur (faster but less detail preservation)
207
- # # smoothed = cv2.GaussianBlur(img, (9, 9), 0)
208
-
209
- # # # Blend with original
210
- # # result = cv2.addWeighted(img, 1-strength, smoothed, strength, 0)
211
- # # return result
212
-
213
-
214
- # # def advanced_skin_smooth(img, strength=0.3):
215
- # # """
216
- # # Advanced skin smoothing with frequency separation
217
- # # Smooths skin while preserving pores and texture
218
-
219
- # # Args:
220
- # # img: Input image (BGR format)
221
- # # strength: Smoothing strength (0.2-0.5 recommended)
222
-
223
- # # Returns:
224
- # # Smoothed image with preserved texture
225
- # # """
226
- # # # Convert to float for better precision
227
- # # img_float = img.astype(np.float32) / 255.0
228
-
229
- # # # Low frequency (color and tone)
230
- # # low_freq = cv2.GaussianBlur(img_float, (0, 0), sigmaX=3, sigmaY=3)
231
-
232
- # # # High frequency (details and texture)
233
- # # high_freq = img_float - low_freq
234
-
235
- # # # Smooth only the low frequency
236
- # # low_freq_smoothed = cv2.bilateralFilter(
237
- # # (low_freq * 255).astype(np.uint8),
238
- # # d=9,
239
- # # sigmaColor=75,
240
- # # sigmaSpace=75
241
- # # ).astype(np.float32) / 255.0
242
-
243
- # # # Blend smoothed low frequency with original
244
- # # low_freq_final = cv2.addWeighted(low_freq, 1-strength, low_freq_smoothed, strength, 0)
245
-
246
- # # # Recombine with high frequency to preserve texture
247
- # # result = low_freq_final + high_freq
248
- # # result = np.clip(result * 255, 0, 255).astype(np.uint8)
249
-
250
- # # return result
251
-
252
-
253
- # # def skin_tone_aware_smooth(img, face_analysis_app, strength=0.3):
254
- # # """
255
- # # Smooth only skin regions (more advanced)
256
- # # Detects face and creates skin mask
257
-
258
- # # Args:
259
- # # img: Input image (BGR format)
260
- # # face_analysis_app: InsightFace app for face detection
261
- # # strength: Smoothing strength
262
-
263
- # # Returns:
264
- # # Image with skin-only smoothing
265
- # # """
266
- # # # Detect faces to create skin mask
267
- # # faces = face_analysis_app.get(img)
268
-
269
- # # if not faces:
270
- # # # No face detected, smooth entire image
271
- # # return subtle_skin_smooth(img, strength)
272
-
273
- # # # Create skin mask based on face regions
274
- # # mask = np.zeros(img.shape[:2], dtype=np.uint8)
275
-
276
- # # for face in faces:
277
- # # x1, y1, x2, y2 = [int(v) for v in face.bbox]
278
-
279
- # # # Expand bbox to include more skin area
280
- # # padding_x = int((x2 - x1) * 0.2)
281
- # # padding_y = int((y2 - y1) * 0.3)
282
-
283
- # # x1 = max(0, x1 - padding_x)
284
- # # y1 = max(0, y1 - padding_y)
285
- # # x2 = min(img.shape[1], x2 + padding_x)
286
- # # y2 = min(img.shape[0], y2 + padding_y)
287
-
288
- # # # Create elliptical mask for natural look
289
- # # center = ((x1 + x2) // 2, (y1 + y2) // 2)
290
- # # axes = ((x2 - x1) // 2, (y2 - y1) // 2)
291
- # # cv2.ellipse(mask, center, axes, 0, 0, 360, 255, -1)
292
-
293
- # # # Blur mask for smooth transition
294
- # # mask = cv2.GaussianBlur(mask, (31, 31), 0)
295
- # # mask_float = mask.astype(float) / 255.0
296
- # # mask_3ch = np.stack([mask_float] * 3, axis=2)
297
-
298
- # # # Apply smoothing
299
- # # smoothed = cv2.bilateralFilter(img, 9, 75, 75)
300
-
301
- # # # Blend only where mask is present
302
- # # result = (smoothed * mask_3ch * strength +
303
- # # img * (1 - mask_3ch * strength)).astype(np.uint8)
304
-
305
- # # return result
306
-
307
- # # def _match_histogram_channel(self, channel, source_vals, target_vals):
308
- # # """Match histogram for single channel"""
309
- # # # Compute CDFs
310
- # # source_hist, _ = np.histogram(source_vals, 256, [0, 256])
311
- # # target_hist, _ = np.histogram(target_vals, 256, [0, 256])
312
-
313
- # # source_cdf = source_hist.cumsum()
314
- # # target_cdf = target_hist.cumsum()
315
-
316
- # # # Normalize
317
- # # source_cdf = source_cdf / source_cdf[-1]
318
- # # target_cdf = target_cdf / target_cdf[-1]
319
-
320
- # # # Create mapping
321
- # # mapping = np.zeros(256, dtype=np.uint8)
322
- # # for i in range(256):
323
- # # # Find closest value in target CDF
324
- # # idx = np.argmin(np.abs(target_cdf - source_cdf[i]))
325
- # # mapping[i] = idx
326
-
327
- # # return mapping[channel]
328
-
329
- # # def seamless_clone_blend(self, source, target, mask, center):
330
- # # """Use Poisson blending for seamless integration"""
331
- # # try:
332
- # # # OpenCV's seamlessClone for natural blending
333
- # # result = cv2.seamlessClone(
334
- # # source, target, mask, center,
335
- # # cv2.NORMAL_CLONE # Try MIXED_CLONE for different effect
336
- # # )
337
- # # return result
338
- # # except:
339
- # # # Fallback to alpha blending if seamlessClone fails
340
- # # return self.alpha_blend_with_feather(source, target, mask)
341
-
342
- # # def alpha_blend_with_feather(self, source, target, mask, feather_amount=15):
343
- # # """Alpha blend with feathered edges for smooth transition"""
344
- # # # Create feathered mask
345
- # # mask_float = mask.astype(float) / 255.0
346
-
347
- # # # Apply Gaussian blur for feathering
348
- # # feathered_mask = cv2.GaussianBlur(mask_float, (feather_amount*2+1, feather_amount*2+1), 0)
349
- # # feathered_mask = np.clip(feathered_mask, 0, 1)
350
-
351
- # # # Expand mask to 3 channels
352
- # # feathered_mask_3ch = np.stack([feathered_mask] * 3, axis=2)
353
-
354
- # # # Blend
355
- # # blended = (source * feathered_mask_3ch +
356
- # # target * (1 - feathered_mask_3ch)).astype(np.uint8)
357
-
358
- # # return blended
359
-
360
- # # def laplacian_pyramid_blend(self, source, target, mask, levels=6):
361
- # # """Multi-resolution blending using Laplacian pyramids"""
362
- # # # Generate Gaussian pyramid for mask
363
- # # mask_float = mask.astype(float) / 255.0
364
- # # gaussian_mask = [mask_float]
365
-
366
- # # for i in range(levels):
367
- # # mask_float = cv2.pyrDown(mask_float)
368
- # # gaussian_mask.append(mask_float)
369
-
370
- # # # Generate Laplacian pyramids
371
- # # def build_laplacian_pyramid(img, levels):
372
- # # gaussian = [img.astype(float)]
373
- # # for i in range(levels):
374
- # # img = cv2.pyrDown(img)
375
- # # gaussian.append(img)
376
-
377
- # # laplacian = []
378
- # # for i in range(levels):
379
- # # size = (gaussian[i].shape[1], gaussian[i].shape[0])
380
- # # upsampled = cv2.pyrUp(gaussian[i + 1], dstsize=size)
381
- # # laplacian.append(gaussian[i] - upsampled)
382
- # # laplacian.append(gaussian[levels])
383
-
384
- # # return laplacian
385
-
386
- # # lp_source = build_laplacian_pyramid(source, levels)
387
- # # lp_target = build_laplacian_pyramid(target, levels)
388
-
389
- # # # Blend each level
390
- # # blended_pyramid = []
391
- # # for ls, lt, gm in zip(lp_source, lp_target, gaussian_mask):
392
- # # # Resize mask if needed
393
- # # if gm.shape[:2] != ls.shape[:2]:
394
- # # gm = cv2.resize(gm, (ls.shape[1], ls.shape[0]))
395
- # # gm_3ch = np.stack([gm] * 3, axis=2)
396
- # # blended = ls * gm_3ch + lt * (1 - gm_3ch)
397
- # # blended_pyramid.append(blended)
398
-
399
- # # # Reconstruct
400
- # # result = blended_pyramid[-1]
401
- # # for i in range(levels - 1, -1, -1):
402
- # # size = (blended_pyramid[i].shape[1], blended_pyramid[i].shape[0])
403
- # # result = cv2.pyrUp(result, dstsize=size)
404
- # # result += blended_pyramid[i]
405
-
406
- # # return np.clip(result, 0, 255).astype(np.uint8)
407
-
408
- # # def match_lighting(self, swapped_face, target_img, face_bbox):
409
- # # """Match lighting conditions between swapped face and target"""
410
- # # x1, y1, x2, y2 = [int(v) for v in face_bbox]
411
-
412
- # # # Extract face region from target
413
- # # target_face = target_img[y1:y2, x1:x2]
414
-
415
- # # if target_face.size == 0 or swapped_face.size == 0:
416
- # # return swapped_face
417
-
418
- # # # Resize if needed
419
- # # if swapped_face.shape[:2] != target_face.shape[:2]:
420
- # # target_face = cv2.resize(target_face,
421
- # # (swapped_face.shape[1], swapped_face.shape[0]))
422
-
423
- # # # Convert to LAB color space
424
- # # swapped_lab = cv2.cvtColor(swapped_face, cv2.COLOR_BGR2LAB).astype(float)
425
- # # target_lab = cv2.cvtColor(target_face, cv2.COLOR_BGR2LAB).astype(float)
426
-
427
- # # # Match mean and std of L channel (luminance)
428
- # # swapped_l = swapped_lab[:, :, 0]
429
- # # target_l = target_lab[:, :, 0]
430
-
431
- # # swapped_l_mean, swapped_l_std = swapped_l.mean(), swapped_l.std()
432
- # # target_l_mean, target_l_std = target_l.mean(), target_l.std()
433
-
434
- # # if swapped_l_std > 0:
435
- # # swapped_lab[:, :, 0] = ((swapped_l - swapped_l_mean) / swapped_l_std *
436
- # # target_l_std + target_l_mean)
437
-
438
- # # # Convert back
439
- # # result = cv2.cvtColor(swapped_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
440
- # # return result
441
-
442
- # # def adjust_face_mask(self, mask, erosion=3, dilation=5):
443
- # # """Adjust mask to avoid harsh edges"""
444
- # # # Slightly erode to avoid edge artifacts
445
- # # kernel_erode = np.ones((erosion, erosion), np.uint8)
446
- # # mask = cv2.erode(mask, kernel_erode, iterations=1)
447
-
448
- # # # Then dilate to smooth
449
- # # kernel_dilate = np.ones((dilation, dilation), np.uint8)
450
- # # mask = cv2.dilate(mask, kernel_dilate, iterations=1)
451
-
452
- # # # Gaussian blur for soft edges
453
- # # mask = cv2.GaussianBlur(mask, (15, 15), 0)
454
-
455
- # # return mask
456
-
457
- # # def natural_face_swap(self, src_img, tgt_img, use_laplacian=True):
458
- # # """
459
- # # Complete natural face swap pipeline
460
-
461
- # # Args:
462
- # # src_img: Source image (RGB)
463
- # # tgt_img: Target image (RGB)
464
- # # use_laplacian: Use Laplacian pyramid blending (slower but better)
465
-
466
- # # Returns:
467
- # # Naturally blended face-swapped image
468
- # # """
469
- # # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
470
- # # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
471
-
472
- # # # Detect faces
473
- # # src_faces = self.face_app.get(src_bgr)
474
- # # tgt_faces = self.face_app.get(tgt_bgr)
475
-
476
- # # if not src_faces or not tgt_faces:
477
- # # raise ValueError("No faces detected")
478
-
479
- # # # Get largest faces
480
- # # src_face = max(src_faces, key=lambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1]))
481
- # # tgt_face = max(tgt_faces, key=lambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1]))
482
-
483
- # # # Perform basic swap
484
- # # swapped_bgr = self.swapper.get(tgt_bgr, tgt_face, src_face, paste_back=True)
485
-
486
- # # # Create face mask
487
- # # x1, y1, x2, y2 = [int(v) for v in tgt_face.bbox]
488
- # # mask = np.zeros(tgt_bgr.shape[:2], dtype=np.uint8)
489
-
490
- # # # Use landmarks for better mask if available
491
- # # if hasattr(tgt_face, 'kps') and tgt_face.kps is not None:
492
- # # kps = tgt_face.kps.astype(np.int32)
493
- # # hull = cv2.convexHull(kps)
494
- # # cv2.fillConvexPoly(mask, hull, 255)
495
- # # else:
496
- # # # Fallback to bbox with some padding
497
- # # padding = int((x2 - x1) * 0.1)
498
- # # cv2.ellipse(mask,
499
- # # ((x1 + x2) // 2, (y1 + y2) // 2),
500
- # # ((x2 - x1) // 2 + padding, (y2 - y1) // 2 + padding),
501
- # # 0, 0, 360, 255, -1)
502
-
503
- # # # Adjust mask for softer edges
504
- # # mask = self.adjust_face_mask(mask)
505
-
506
- # # # Color histogram matching
507
- # # swapped_bgr = self.match_color_histogram(swapped_bgr, tgt_bgr, mask)
508
-
509
- # # # Lighting adjustment
510
- # # swapped_face_region = swapped_bgr[y1:y2, x1:x2]
511
- # # adjusted_face = self.match_lighting(swapped_face_region, tgt_bgr, tgt_face.bbox)
512
- # # swapped_bgr[y1:y2, x1:x2] = adjusted_face
513
-
514
- # # # Final blending
515
- # # if use_laplacian:
516
- # # # Best quality but slower
517
- # # result = self.laplacian_pyramid_blend(swapped_bgr, tgt_bgr, mask)
518
- # # else:
519
- # # # Faster alternative: Seamless cloning
520
- # # center = ((x1 + x2) // 2, (y1 + y2) // 2)
521
- # # result = self.seamless_clone_blend(swapped_bgr, tgt_bgr, mask, center)
522
-
523
- # # return cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
524
-
525
-
526
- # # # ============================================
527
- # # # Integration into your existing code
528
- # # # ============================================
529
-
530
- # # def enhanced_face_swap_and_enhance(src_img, tgt_img, swapper, face_app, temp_dir=None):
531
- # # """
532
- # # Enhanced version of your face_swap_and_enhance function
533
- # # """
534
- # # try:
535
- # # # Initialize natural swapper
536
- # # natural_swapper = NaturalFaceSwapper(swapper, face_app)
537
-
538
- # # # Perform natural swap
539
- # # swapped_rgb = natural_swapper.natural_face_swap(
540
- # # src_img, tgt_img,
541
- # # use_laplacian=True # Set False for faster processing
542
- # # )
543
-
544
- # # # Apply CodeFormer enhancement
545
- # # enhanced_rgb = enhance_image_with_codeformer(swapped_rgb, temp_dir)
546
-
547
- # # # Post-enhancement sharpening (subtle)
548
- # # kernel_sharpen = np.array([[-0.5, -0.5, -0.5],
549
- # # [-0.5, 5.0, -0.5],
550
- # # [-0.5, -0.5, -0.5]]) * 0.3
551
- # # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
552
- # # sharpened = cv2.filter2D(enhanced_bgr, -1, kernel_sharpen)
553
-
554
- # # # Blend sharpened with original (60% sharp, 40% original)
555
- # # final_bgr = cv2.addWeighted(sharpened, 0.6, enhanced_bgr, 0.4, 0)
556
- # # final_rgb = cv2.cvtColor(final_bgr, cv2.COLOR_BGR2RGB)
557
-
558
- # # # Save result
559
- # # if temp_dir is None:
560
- # # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_{uuid.uuid4().hex[:8]}")
561
- # # os.makedirs(temp_dir, exist_ok=True)
562
-
563
- # # final_path = os.path.join(temp_dir, "enhanced.png")
564
- # # cv2.imwrite(final_path, final_bgr)
565
-
566
- # # return final_rgb, final_path, ""
567
-
568
- # # except Exception as e:
569
- # # return None, None, f"❌ Error: {str(e)}"
570
-
571
- # # --------------------- FastAPI ---------------------
572
- # fastapi_app = FastAPI()
573
-
574
- # @fastapi_app.on_event("startup")
575
- # async def startup_db():
576
- # global client, database
577
- # if MONGODB_URL:
578
- # try:
579
- # logger.info("Initializing MongoDB for API logs...")
580
- # client = AsyncIOMotorClient(MONGODB_URL)
581
- # database = client.FaceSwap
582
- # logger.info("MongoDB initialized for API logs")
583
- # except Exception as e:
584
- # logger.warning(f"MongoDB connection failed (optional): {e}")
585
- # client = None
586
- # database = None
587
- # else:
588
- # logger.warning("MONGODB_URL not set, skipping MongoDB initialization")
589
-
590
- # @fastapi_app.on_event("shutdown")
591
- # async def shutdown_db():
592
- # global client
593
- # if client:
594
- # client.close()
595
- # logger.info("MongoDB connection closed")
596
-
597
- # # --------------------- Auth ---------------------
598
- # security = HTTPBearer()
599
-
600
- # def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
601
- # if credentials.credentials != API_SECRET_TOKEN:
602
- # raise HTTPException(status_code=401, detail="Invalid or missing token")
603
- # return credentials.credentials
604
-
605
- # # --------------------- Logging API Hits ---------------------
606
- # async def log_faceswap_hit(token: str, status: str = "success"):
607
- # global database
608
- # if database is None:
609
- # return
610
- # await database.api_logs.insert_one({
611
- # "token": token,
612
- # "endpoint": "/faceswap",
613
- # "status": status,
614
- # "timestamp": datetime.utcnow()
615
- # })
616
-
617
- # # --------------------- Face Swap Pipeline ---------------------
618
- # swap_lock = threading.Lock()
619
-
620
- # def enhance_image_with_codeformer(rgb_img, temp_dir=None):
621
- # if temp_dir is None:
622
- # temp_dir = os.path.join(tempfile.gettempdir(), f"enhance_{uuid.uuid4().hex[:8]}")
623
- # os.makedirs(temp_dir, exist_ok=True)
624
-
625
- # input_path = os.path.join(temp_dir, "input.jpg")
626
- # cv2.imwrite(input_path, cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR))
627
-
628
- # python_cmd = sys.executable if sys.executable else "python3"
629
- # cmd = (
630
- # f"{python_cmd} {CODEFORMER_PATH} "
631
- # f"-w 0.7 "
632
- # f"--input_path {input_path} "
633
- # f"--output_path {temp_dir} "
634
- # f"--bg_upsampler realesrgan "
635
- # f"--face_upsample"
636
- # )
637
-
638
- # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
639
- # if result.returncode != 0:
640
- # raise RuntimeError(result.stderr)
641
-
642
- # final_dir = os.path.join(temp_dir, "final_results")
643
- # files = [f for f in os.listdir(final_dir) if f.endswith(".png")]
644
- # if not files:
645
- # raise RuntimeError("No enhanced output")
646
-
647
- # final_path = os.path.join(final_dir, files[0])
648
- # enhanced = cv2.imread(final_path)
649
- # return cv2.cvtColor(enhanced, cv2.COLOR_BGR2RGB)
650
-
651
- # def multi_face_swap(src_img, tgt_img):
652
- # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
653
- # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
654
-
655
- # src_faces = face_analysis_app.get(src_bgr)
656
- # tgt_faces = face_analysis_app.get(tgt_bgr)
657
-
658
- # if not src_faces or not tgt_faces:
659
- # raise ValueError("No faces detected")
660
-
661
- # def face_sort_key(face):
662
- # x1, y1, x2, y2 = face.bbox
663
- # area = (x2 - x1) * (y2 - y1)
664
- # cx = (x1 + x2) / 2
665
- # return (-area, cx)
666
-
667
- # # Split by gender
668
- # src_male = [f for f in src_faces if f.gender == 1]
669
- # src_female = [f for f in src_faces if f.gender == 0]
670
-
671
- # tgt_male = [f for f in tgt_faces if f.gender == 1]
672
- # tgt_female = [f for f in tgt_faces if f.gender == 0]
673
-
674
- # # Sort inside gender groups
675
- # src_male = sorted(src_male, key=face_sort_key)
676
- # src_female = sorted(src_female, key=face_sort_key)
677
-
678
- # tgt_male = sorted(tgt_male, key=face_sort_key)
679
- # tgt_female = sorted(tgt_female, key=face_sort_key)
680
-
681
- # # Build final swap pairs
682
- # pairs = []
683
-
684
- # for s, t in zip(src_male, tgt_male):
685
- # pairs.append((s, t))
686
-
687
- # for s, t in zip(src_female, tgt_female):
688
- # pairs.append((s, t))
689
-
690
- # # Fallback if gender mismatch
691
- # if not pairs:
692
- # src_faces = sorted(src_faces, key=face_sort_key)
693
- # tgt_faces = sorted(tgt_faces, key=face_sort_key)
694
- # pairs = list(zip(src_faces, tgt_faces))
695
-
696
- # result_img = tgt_bgr.copy()
697
-
698
- # for src_face, _ in pairs:
699
- # # 🔁 re-detect current target faces
700
- # if face_analysis_app is None:
701
- # raise ValueError("Face analysis models not initialized. Please ensure models are downloaded.")
702
- # current_faces = face_analysis_app.get(result_img)
703
- # current_faces = sorted(current_faces, key=face_sort_key)
704
-
705
- # # choose best matching gender
706
- # candidates = [
707
- # f for f in current_faces if f.gender == src_face.gender
708
- # ] or current_faces
709
-
710
- # target_face = candidates[0]
711
-
712
- # if swapper is None:
713
- # raise ValueError("Face swap models not initialized. Please ensure models are downloaded.")
714
- # result_img = swapper.get(
715
- # result_img,
716
- # target_face,
717
- # src_face,
718
- # paste_back=True
719
- # )
720
-
721
- # return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
722
-
723
-
724
-
725
- # def face_swap_and_enhance(src_img, tgt_img, temp_dir=None):
726
- # try:
727
- # with swap_lock:
728
- # # Use a temp dir for intermediate files
729
- # if temp_dir is None:
730
- # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_work_{uuid.uuid4().hex[:8]}")
731
- # if os.path.exists(temp_dir):
732
- # shutil.rmtree(temp_dir)
733
- # os.makedirs(temp_dir, exist_ok=True)
734
-
735
- # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
736
- # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
737
-
738
- # src_faces = face_analysis_app.get(src_bgr)
739
- # tgt_faces = face_analysis_app.get(tgt_bgr)
740
- # if face_analysis_app is None:
741
- # return None, None, "❌ Face analysis models not initialized. Please ensure models are downloaded."
742
- # if not src_faces or not tgt_faces:
743
- # return None, None, "❌ Face not detected in one of the images"
744
-
745
- # swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
746
- # if swapper is None:
747
- # return None, None, "❌ Face swap models not initialized. Please ensure models are downloaded."
748
- # swapped_bgr = swapper.get(tgt_bgr, tgt_faces[0], src_faces[0])
749
- # if swapped_bgr is None:
750
- # return None, None, "❌ Face swap failed"
751
-
752
- # cv2.imwrite(swapped_path, swapped_bgr)
753
-
754
- # python_cmd = sys.executable if sys.executable else "python3"
755
- # cmd = f"{python_cmd} {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
756
- # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
757
- # if result.returncode != 0:
758
- # return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
759
-
760
- # final_results_dir = os.path.join(temp_dir, "final_results")
761
- # final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
762
- # if not final_files:
763
- # return None, None, "❌ No enhanced image found"
764
-
765
- # final_path = os.path.join(final_results_dir, final_files[0])
766
- # final_img_bgr = cv2.imread(final_path)
767
- # if final_img_bgr is None:
768
- # return None, None, "❌ Failed to read enhanced image file"
769
- # final_img = cv2.cvtColor(final_img_bgr, cv2.COLOR_BGR2RGB)
770
-
771
- # return final_img, final_path, ""
772
-
773
- # except Exception as e:
774
- # return None, None, f"❌ Error: {str(e)}"
775
-
776
- # def compress_image(
777
- # image_bytes: bytes,
778
- # max_size=(1280, 1280), # max width/height
779
- # quality=75 # JPEG quality (60–80 is ideal)
780
- # ) -> bytes:
781
- # """
782
- # Compress image by resizing and lowering quality.
783
- # Returns compressed image bytes.
784
- # """
785
- # img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
786
-
787
- # # Resize while maintaining aspect ratio
788
- # img.thumbnail(max_size, Image.LANCZOS)
789
-
790
- # output = io.BytesIO()
791
- # img.save(
792
- # output,
793
- # format="JPEG",
794
- # quality=quality,
795
- # optimize=True,
796
- # progressive=True
797
- # )
798
-
799
- # return output.getvalue()
800
-
801
- # # --------------------- DigitalOcean Spaces Helper ---------------------
802
- # def get_spaces_client():
803
- # session = boto3.session.Session()
804
- # client = session.client(
805
- # 's3',
806
- # region_name=DO_SPACES_REGION,
807
- # endpoint_url=DO_SPACES_ENDPOINT,
808
- # aws_access_key_id=DO_SPACES_KEY,
809
- # aws_secret_access_key=DO_SPACES_SECRET,
810
- # config=Config(signature_version='s3v4')
811
- # )
812
- # return client
813
-
814
- # def upload_to_spaces(file_bytes, key, content_type="image/png"):
815
- # client = get_spaces_client()
816
- # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
817
- # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
818
-
819
- # def download_from_spaces(key):
820
- # client = get_spaces_client()
821
- # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
822
- # return obj['Body'].read()
823
-
824
- # def build_multi_faceswap_gradio():
825
- # with gr.Blocks() as demo:
826
- # gr.Markdown("## 👩‍❤️‍👨 Multi Face Swap (Couple → Couple)")
827
-
828
- # with gr.Row():
829
- # src = gr.Image(type="numpy", label="Source Image (2 Faces)")
830
- # tgt = gr.Image(type="numpy", label="Target Image (2 Faces)")
831
-
832
- # out = gr.Image(type="numpy", label="Swapped Result")
833
- # error = gr.Textbox(label="Logs", interactive=False)
834
-
835
- # def process(src_img, tgt_img):
836
- # try:
837
- # swapped = multi_face_swap(src_img, tgt_img)
838
- # enhanced = enhance_image_with_codeformer(swapped)
839
- # return enhanced, ""
840
- # except Exception as e:
841
- # return None, str(e)
842
-
843
- # btn = gr.Button("Swap Faces")
844
- # btn.click(process, [src, tgt], [out, error])
845
-
846
- # return demo
847
-
848
- # def mandatory_enhancement(rgb_img):
849
- # """
850
- # Always runs CodeFormer on the final image.
851
- # Fail-safe: returns original if enhancement fails.
852
- # """
853
- # try:
854
- # return enhance_image_with_codeformer(rgb_img)
855
- # except Exception as e:
856
- # logger.error(f"CodeFormer failed, returning original: {e}")
857
- # return rgb_img
858
-
859
- # # --------------------- API Endpoints ---------------------
860
- # @fastapi_app.get("/")
861
- # async def root():
862
- # """Root endpoint"""
863
- # return {
864
- # "success": True,
865
- # "message": "FaceSwap API",
866
- # "data": {
867
- # "version": "1.0.0",
868
- # "Product Name":"Beauty Camera - GlowCam AI Studio",
869
- # "Released By" : "LogicGo Infotech"
870
- # }
871
- # }
872
- # @fastapi_app.get("/health")
873
- # async def health():
874
- # return {"status": "healthy"}
875
-
876
- # from fastapi import Form
877
- # import requests
878
- # @fastapi_app.get("/test-admin-db")
879
- # async def test_admin_db():
880
- # try:
881
- # doc = await admin_db.list_collection_names()
882
- # return {"ok": True, "collections": doc}
883
- # except Exception as e:
884
- # return {"ok": False, "error": str(e), "url": ADMIN_MONGO_URL}
885
-
886
- # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
887
- # async def face_swap_api(
888
- # source: UploadFile = File(...),
889
- # target_category_id: str = Form(None),
890
- # new_category_id: str = Form(None),
891
- # user_id: Optional[str] = Form(None),
892
- # credentials: HTTPAuthorizationCredentials = Security(security)
893
- # ):
894
- # start_time = datetime.utcnow()
895
-
896
- # try:
897
- # # ------------------------------------------------------------------
898
- # # VALIDATION
899
- # # ------------------------------------------------------------------
900
- # # --------------------------------------------------------------
901
- # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
902
- # # --------------------------------------------------------------
903
- # if target_category_id == "":
904
- # target_category_id = None
905
-
906
- # if new_category_id == "":
907
- # new_category_id = None
908
-
909
- # if user_id == "":
910
- # user_id = None
911
-
912
- # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
913
-
914
- # if target_category_id and new_category_id:
915
- # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
916
-
917
- # if not target_category_id and not new_category_id:
918
- # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
919
-
920
- # # ------------------------------------------------------------------
921
- # # READ SOURCE IMAGE
922
- # # ------------------------------------------------------------------
923
- # src_bytes = await source.read()
924
- # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
925
- # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
926
-
927
- # # ------------------------------------------------------------------
928
- # # CASE 1 : new_category_id → MongoDB lookup
929
- # # ------------------------------------------------------------------
930
- # if new_category_id:
931
-
932
- # doc = await subcategories_col.find_one({
933
- # "asset_images._id": ObjectId(new_category_id)
934
- # })
935
-
936
- # if not doc:
937
- # raise HTTPException(404, "Asset image not found in database")
938
-
939
- # # extract correct asset
940
- # asset = next(
941
- # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
942
- # None
943
- # )
944
-
945
- # if not asset:
946
- # raise HTTPException(404, "Asset image URL not found")
947
-
948
- # # correct URL
949
- # target_url = asset["url"]
950
-
951
- # # correct categoryId (ObjectId)
952
- # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
953
- # subcategory_oid = doc["_id"]
954
-
955
- # # ------------------------------------------------------------------#
956
- # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
957
- # # ------------------------------------------------------------------#
958
- # if user_id:
959
- # try:
960
- # user_id_clean = user_id.strip()
961
- # if not user_id_clean:
962
- # raise ValueError("user_id cannot be empty")
963
- # try:
964
- # user_oid = ObjectId(user_id_clean)
965
- # except (InvalidId, ValueError) as e:
966
- # logger.error(f"Invalid user_id format: {user_id_clean}")
967
- # raise ValueError(f"Invalid user_id format: {user_id_clean}")
968
-
969
- # now = datetime.utcnow()
970
-
971
- # # Normalize dates (UTC midnight)
972
- # today_date = datetime(now.year, now.month, now.day)
973
-
974
- # # -------------------------------------------------
975
- # # STEP 1: Ensure root document exists
976
- # # -------------------------------------------------
977
- # await media_clicks_col.update_one(
978
- # {"userId": user_oid},
979
- # {
980
- # "$setOnInsert": {
981
- # "userId": user_oid,
982
- # "createdAt": now,
983
- # "ai_edit_complete": 0,
984
- # "ai_edit_daily_count": []
985
- # }
986
- # },
987
- # upsert=True
988
- # )
989
- # # -------------------------------------------------
990
- # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
991
- # # -------------------------------------------------
992
- # doc = await media_clicks_col.find_one(
993
- # {"userId": user_oid},
994
- # {"ai_edit_daily_count": 1}
995
- # )
996
-
997
- # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
998
-
999
- # # Normalize today to UTC midnight
1000
- # today_date = datetime(now.year, now.month, now.day)
1001
-
1002
- # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
1003
- # daily_map = {}
1004
- # for entry in daily_entries:
1005
- # d = entry["date"]
1006
- # if isinstance(d, datetime):
1007
- # d = datetime(d.year, d.month, d.day)
1008
- # daily_map[d] = entry["count"] # overwrite = no duplicates
1009
-
1010
- # # Determine last recorded date
1011
- # last_date = max(daily_map.keys()) if daily_map else today_date
1012
-
1013
- # # Fill ALL missing days with count = 0
1014
- # next_day = last_date + timedelta(days=1)
1015
- # while next_day < today_date:
1016
- # daily_map.setdefault(next_day, 0)
1017
- # next_day += timedelta(days=1)
1018
-
1019
- # # Mark today as used (binary)
1020
- # daily_map[today_date] = 1
1021
-
1022
- # # Rebuild list: OLDEST → NEWEST
1023
- # final_daily_entries = [
1024
- # {"date": d, "count": daily_map[d]}
1025
- # for d in sorted(daily_map.keys())
1026
- # ]
1027
-
1028
- # # Keep only last 32 days
1029
- # final_daily_entries = final_daily_entries[-32:]
1030
-
1031
- # # Atomic replace
1032
- # await media_clicks_col.update_one(
1033
- # {"userId": user_oid},
1034
- # {
1035
- # "$set": {
1036
- # "ai_edit_daily_count": final_daily_entries,
1037
- # "updatedAt": now
1038
- # }
1039
- # }
1040
- # )
1041
-
1042
- # # -------------------------------------------------
1043
- # # STEP 3: Try updating existing subCategory
1044
- # # -------------------------------------------------
1045
- # update_result = await media_clicks_col.update_one(
1046
- # {
1047
- # "userId": user_oid,
1048
- # "subCategories.subCategoryId": subcategory_oid
1049
- # },
1050
- # {
1051
- # "$inc": {
1052
- # "subCategories.$.click_count": 1,
1053
- # "ai_edit_complete": 1
1054
- # },
1055
- # "$set": {
1056
- # "subCategories.$.lastClickedAt": now,
1057
- # "ai_edit_last_date": now,
1058
- # "updatedAt": now
1059
- # }
1060
- # }
1061
- # )
1062
-
1063
- # # -------------------------------------------------
1064
- # # STEP 4: Push subCategory if missing
1065
- # # -------------------------------------------------
1066
- # if update_result.matched_count == 0:
1067
- # await media_clicks_col.update_one(
1068
- # {"userId": user_oid},
1069
- # {
1070
- # "$inc": {
1071
- # "ai_edit_complete": 1
1072
- # },
1073
- # "$set": {
1074
- # "ai_edit_last_date": now,
1075
- # "updatedAt": now
1076
- # },
1077
- # "$push": {
1078
- # "subCategories": {
1079
- # "subCategoryId": subcategory_oid,
1080
- # "click_count": 1,
1081
- # "lastClickedAt": now
1082
- # }
1083
- # }
1084
- # }
1085
- # )
1086
-
1087
- # # -------------------------------------------------
1088
- # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
1089
- # # -------------------------------------------------
1090
- # user_doc = await media_clicks_col.find_one({"userId": user_oid})
1091
- # if user_doc and "subCategories" in user_doc:
1092
- # subcategories = user_doc["subCategories"]
1093
- # # Sort by lastClickedAt in ascending order (oldest first)
1094
- # # Handle missing or None dates by using datetime.min
1095
- # subcategories_sorted = sorted(
1096
- # subcategories,
1097
- # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
1098
- # )
1099
- # # Update with sorted array
1100
- # await media_clicks_col.update_one(
1101
- # {"userId": user_oid},
1102
- # {
1103
- # "$set": {
1104
- # "subCategories": subcategories_sorted,
1105
- # "updatedAt": now
1106
- # }
1107
- # }
1108
- # )
1109
-
1110
- # logger.info(
1111
- # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
1112
- # user_id,
1113
- # str(subcategory_oid)
1114
- # )
1115
-
1116
- # except Exception as media_err:
1117
- # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
1118
-
1119
- # # # ------------------------------------------------------------------
1120
- # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
1121
- # # # ------------------------------------------------------------------
1122
- # if target_category_id:
1123
- # client = get_spaces_client()
1124
- # base_prefix = "faceswap/target/"
1125
- # resp = client.list_objects_v2(
1126
- # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
1127
- # )
1128
-
1129
- # # Extract categories from the CommonPrefixes
1130
- # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
1131
-
1132
- # target_url = None
1133
-
1134
- # # --- FIX STARTS HERE ---
1135
- # for category in categories:
1136
- # original_prefix = f"faceswap/target/{category}/original/"
1137
- # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
1138
-
1139
- # # List objects in original/
1140
- # original_objects = client.list_objects_v2(
1141
- # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
1142
- # ).get("Contents", [])
1143
-
1144
- # # List objects in thumb/ (optional: for the old code's extra check)
1145
- # thumb_objects = client.list_objects_v2(
1146
- # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
1147
- # ).get("Contents", [])
1148
-
1149
- # # Extract only the filenames and filter for .png
1150
- # original_filenames = sorted([
1151
- # obj["Key"].split("/")[-1] for obj in original_objects
1152
- # if obj["Key"].split("/")[-1].endswith(".png")
1153
- # ])
1154
- # thumb_filenames = [
1155
- # obj["Key"].split("/")[-1] for obj in thumb_objects
1156
- # ]
1157
-
1158
- # # Replicate the old indexing logic based on sorted filenames
1159
- # for idx, filename in enumerate(original_filenames, start=1):
1160
- # cid = f"{category.lower()}image_{idx}"
1161
-
1162
- # # Optional: Replicate the thumb file check for 100% parity
1163
- # # if filename in thumb_filenames and cid == target_category_id:
1164
- # # Simpler check just on the ID, assuming thumb files are present
1165
- # if cid == target_category_id:
1166
- # # Construct the final target URL using the full prefix and the filename
1167
- # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
1168
- # break
1169
-
1170
- # if target_url:
1171
- # break
1172
- # # --- FIX ENDS HERE ---
1173
-
1174
- # if not target_url:
1175
- # raise HTTPException(404, "Target categoryId not found")
1176
- # # # ------------------------------------------------------------------
1177
- # # # DOWNLOAD TARGET IMAGE
1178
- # # # ------------------------------------------------------------------
1179
- # async with httpx.AsyncClient(timeout=30.0) as client:
1180
- # response = await client.get(target_url)
1181
- # response.raise_for_status()
1182
- # tgt_bytes = response.content
1183
-
1184
- # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
1185
- # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
1186
-
1187
- # if src_bgr is None or tgt_bgr is None:
1188
- # raise HTTPException(400, "Invalid image data")
1189
-
1190
- # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
1191
- # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
1192
-
1193
- # # ------------------------------------------------------------------
1194
- # # FACE SWAP EXECUTION
1195
- # # ------------------------------------------------------------------
1196
- # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
1197
-
1198
- # # #--------------------Version 2.0 ----------------------------------------#
1199
- # # final_img, final_path, err = enhanced_face_swap_and_enhance(src_rgb, tgt_rgb)
1200
- # # #--------------------Version 2.0 ----------------------------------------#
1201
-
1202
- # if err:
1203
- # raise HTTPException(500, err)
1204
-
1205
- # with open(final_path, "rb") as f:
1206
- # result_bytes = f.read()
1207
-
1208
- # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
1209
- # result_url = upload_to_spaces(result_bytes, result_key)
1210
- # # -------------------------------------------------
1211
- # # COMPRESS IMAGE (2–3 MB target)
1212
- # # -------------------------------------------------
1213
- # compressed_bytes = compress_image(
1214
- # image_bytes=result_bytes,
1215
- # max_size=(1280, 1280),
1216
- # quality=72
1217
- # )
1218
-
1219
- # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
1220
- # compressed_url = upload_to_spaces(
1221
- # compressed_bytes,
1222
- # compressed_key,
1223
- # content_type="image/jpeg"
1224
- # )
1225
- # end_time = datetime.utcnow()
1226
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1227
-
1228
- # if database is not None:
1229
- # await database.api_logs.insert_one({
1230
- # "endpoint": "/face-swap",
1231
- # "status": "success",
1232
- # "response_time_ms": response_time_ms,
1233
- # "timestamp": end_time
1234
- # })
1235
-
1236
-
1237
- # return {
1238
- # "result_key": result_key,
1239
- # "result_url": result_url,
1240
- # "Compressed_Image_URL": compressed_url
1241
- # }
1242
-
1243
- # except Exception as e:
1244
- # end_time = datetime.utcnow()
1245
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1246
-
1247
- # if database is not None:
1248
- # await database.api_logs.insert_one({
1249
- # "endpoint": "/face-swap",
1250
- # "status": "fail",
1251
- # "response_time_ms": response_time_ms,
1252
- # "timestamp": end_time,
1253
- # "error": str(e)
1254
- # })
1255
-
1256
- # raise HTTPException(500, f"Face swap failed: {str(e)}")
1257
-
1258
- # @fastapi_app.get("/preview/{result_key:path}")
1259
- # async def preview_result(result_key: str):
1260
- # try:
1261
- # img_bytes = download_from_spaces(result_key)
1262
- # except Exception:
1263
- # raise HTTPException(status_code=404, detail="Result not found")
1264
- # return Response(
1265
- # content=img_bytes,
1266
- # media_type="image/png",
1267
- # headers={"Content-Disposition": "inline; filename=result.png"}
1268
- # )
1269
-
1270
- # @fastapi_app.post("/multi-face-swap", dependencies=[Depends(verify_token)])
1271
- # async def multi_face_swap_api(
1272
- # source_image: UploadFile = File(...),
1273
- # target_image: UploadFile = File(...)
1274
- # ):
1275
- # start_time = datetime.utcnow()
1276
-
1277
- # try:
1278
- # # -----------------------------
1279
- # # Read images
1280
- # # -----------------------------
1281
- # src_bytes = await source_image.read()
1282
- # tgt_bytes = await target_image.read()
1283
-
1284
- # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
1285
- # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
1286
-
1287
- # if src_bgr is None or tgt_bgr is None:
1288
- # raise HTTPException(400, "Invalid image data")
1289
-
1290
- # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
1291
- # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
1292
-
1293
- # # -----------------------------
1294
- # # Multi-face swap
1295
- # # -----------------------------
1296
- # swapped_rgb = multi_face_swap(src_rgb, tgt_rgb)
1297
-
1298
- # # -----------------------------
1299
- # # 🔥 MANDATORY ENHANCEMENT
1300
- # # -----------------------------
1301
- # final_rgb = mandatory_enhancement(swapped_rgb)
1302
-
1303
- # final_bgr = cv2.cvtColor(final_rgb, cv2.COLOR_RGB2BGR)
1304
-
1305
- # # -----------------------------
1306
- # # Save temp result
1307
- # # -----------------------------
1308
- # temp_dir = tempfile.mkdtemp(prefix="multi_faceswap_")
1309
- # result_path = os.path.join(temp_dir, "result.png")
1310
- # cv2.imwrite(result_path, final_bgr)
1311
-
1312
- # with open(result_path, "rb") as f:
1313
- # result_bytes = f.read()
1314
-
1315
- # # -----------------------------
1316
- # # Upload
1317
- # # -----------------------------
1318
- # result_key = f"faceswap/multi/{uuid.uuid4().hex}.png"
1319
- # result_url = upload_to_spaces(
1320
- # result_bytes,
1321
- # result_key,
1322
- # content_type="image/png"
1323
- # )
1324
-
1325
- # return {
1326
- # "result_key": result_key,
1327
- # "result_url": result_url
1328
- # }
1329
-
1330
- # except Exception as e:
1331
- # raise HTTPException(status_code=500, detail=str(e))
1332
-
1333
-
1334
- # @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
1335
- # async def face_swap_api(
1336
- # image1: UploadFile = File(...),
1337
- # image2: Optional[UploadFile] = File(None),
1338
- # target_category_id: str = Form(None),
1339
- # new_category_id: str = Form(None),
1340
- # user_id: Optional[str] = Form(None),
1341
- # credentials: HTTPAuthorizationCredentials = Security(security)
1342
- # ):
1343
- # """
1344
- # Production-ready face swap endpoint supporting:
1345
- # - Multiple source images (image1 + optional image2)
1346
- # - Gender-based pairing
1347
- # - Merged faces from multiple sources
1348
- # - Mandatory CodeFormer enhancement
1349
- # """
1350
- # start_time = datetime.utcnow()
1351
-
1352
- # try:
1353
- # # -----------------------------
1354
- # # Validate input
1355
- # # -----------------------------
1356
- # if target_category_id == "":
1357
- # target_category_id = None
1358
- # if new_category_id == "":
1359
- # new_category_id = None
1360
- # if user_id == "":
1361
- # user_id = None
1362
-
1363
- # if target_category_id and new_category_id:
1364
- # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
1365
- # if not target_category_id and not new_category_id:
1366
- # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
1367
-
1368
- # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
1369
-
1370
- # # -----------------------------
1371
- # # Read source images
1372
- # # -----------------------------
1373
- # src_images = []
1374
- # img1_bytes = await image1.read()
1375
- # src1 = cv2.imdecode(np.frombuffer(img1_bytes, np.uint8), cv2.IMREAD_COLOR)
1376
- # if src1 is None:
1377
- # raise HTTPException(400, "Invalid image1 data")
1378
- # src_images.append(cv2.cvtColor(src1, cv2.COLOR_BGR2RGB))
1379
-
1380
- # if image2:
1381
- # img2_bytes = await image2.read()
1382
- # src2 = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
1383
- # if src2 is not None:
1384
- # src_images.append(cv2.cvtColor(src2, cv2.COLOR_BGR2RGB))
1385
-
1386
- # # -----------------------------
1387
- # # Resolve target image
1388
- # # -----------------------------
1389
- # target_url = None
1390
- # if new_category_id:
1391
- # doc = await subcategories_col.find_one({
1392
- # "asset_images._id": ObjectId(new_category_id)
1393
- # })
1394
-
1395
- # if not doc:
1396
- # raise HTTPException(404, "Asset image not found in database")
1397
-
1398
- # asset = next(
1399
- # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
1400
- # None
1401
- # )
1402
-
1403
- # if not asset:
1404
- # raise HTTPException(404, "Asset image URL not found")
1405
-
1406
- # target_url = asset["url"]
1407
- # subcategory_oid = doc["_id"]
1408
-
1409
- # if user_id:
1410
- # try:
1411
- # user_id_clean = user_id.strip()
1412
- # if not user_id_clean:
1413
- # raise ValueError("user_id cannot be empty")
1414
- # try:
1415
- # user_oid = ObjectId(user_id_clean)
1416
- # except (InvalidId, ValueError):
1417
- # logger.error(f"Invalid user_id format: {user_id_clean}")
1418
- # raise ValueError(f"Invalid user_id format: {user_id_clean}")
1419
-
1420
- # now = datetime.utcnow()
1421
-
1422
- # # Step 1: ensure root document exists
1423
- # await media_clicks_col.update_one(
1424
- # {"userId": user_oid},
1425
- # {
1426
- # "$setOnInsert": {
1427
- # "userId": user_oid,
1428
- # "createdAt": now,
1429
- # "ai_edit_complete": 0,
1430
- # "ai_edit_daily_count": []
1431
- # }
1432
- # },
1433
- # upsert=True
1434
- # )
1435
-
1436
- # # Step 2: handle daily usage (binary, no duplicates)
1437
- # doc = await media_clicks_col.find_one(
1438
- # {"userId": user_oid},
1439
- # {"ai_edit_daily_count": 1}
1440
- # )
1441
-
1442
- # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
1443
-
1444
- # today_date = datetime(now.year, now.month, now.day)
1445
-
1446
- # daily_map = {}
1447
- # for entry in daily_entries:
1448
- # d = entry["date"]
1449
- # if isinstance(d, datetime):
1450
- # d = datetime(d.year, d.month, d.day)
1451
- # daily_map[d] = entry["count"]
1452
-
1453
- # last_date = max(daily_map.keys()) if daily_map else None
1454
-
1455
- # if last_date != today_date:
1456
- # daily_map[today_date] = 1
1457
-
1458
- # final_daily_entries = [
1459
- # {"date": d, "count": daily_map[d]}
1460
- # for d in sorted(daily_map.keys())
1461
- # ]
1462
-
1463
- # final_daily_entries = final_daily_entries[-32:]
1464
-
1465
- # await media_clicks_col.update_one(
1466
- # {"userId": user_oid},
1467
- # {
1468
- # "$set": {
1469
- # "ai_edit_daily_count": final_daily_entries,
1470
- # "updatedAt": now
1471
- # }
1472
- # }
1473
- # )
1474
-
1475
- # # Step 3: try updating existing subCategory
1476
- # update_result = await media_clicks_col.update_one(
1477
- # {
1478
- # "userId": user_oid,
1479
- # "subCategories.subCategoryId": subcategory_oid
1480
- # },
1481
- # {
1482
- # "$inc": {
1483
- # "subCategories.$.click_count": 1,
1484
- # "ai_edit_complete": 1
1485
- # },
1486
- # "$set": {
1487
- # "subCategories.$.lastClickedAt": now,
1488
- # "ai_edit_last_date": now,
1489
- # "updatedAt": now
1490
- # }
1491
- # }
1492
- # )
1493
-
1494
- # # Step 4: push subCategory if missing
1495
- # if update_result.matched_count == 0:
1496
- # await media_clicks_col.update_one(
1497
- # {"userId": user_oid},
1498
- # {
1499
- # "$inc": {
1500
- # "ai_edit_complete": 1
1501
- # },
1502
- # "$set": {
1503
- # "ai_edit_last_date": now,
1504
- # "updatedAt": now
1505
- # },
1506
- # "$push": {
1507
- # "subCategories": {
1508
- # "subCategoryId": subcategory_oid,
1509
- # "click_count": 1,
1510
- # "lastClickedAt": now
1511
- # }
1512
- # }
1513
- # }
1514
- # )
1515
-
1516
- # # Step 5: sort subCategories by lastClickedAt (ascending)
1517
- # user_doc = await media_clicks_col.find_one({"userId": user_oid})
1518
- # if user_doc and "subCategories" in user_doc:
1519
- # subcategories = user_doc["subCategories"]
1520
- # subcategories_sorted = sorted(
1521
- # subcategories,
1522
- # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
1523
- # )
1524
- # await media_clicks_col.update_one(
1525
- # {"userId": user_oid},
1526
- # {
1527
- # "$set": {
1528
- # "subCategories": subcategories_sorted,
1529
- # "updatedAt": now
1530
- # }
1531
- # }
1532
- # )
1533
-
1534
- # logger.info(
1535
- # "[MEDIA_CLICK] user=%s subCategory=%s ai_edit_complete++ daily_tracked",
1536
- # user_id,
1537
- # str(subcategory_oid)
1538
- # )
1539
-
1540
- # except Exception as media_err:
1541
- # logger.error(f"MEDIA_CLICK ERROR: {media_err}")
1542
-
1543
- # if target_category_id:
1544
- # client = get_spaces_client()
1545
- # base_prefix = "faceswap/target/"
1546
- # resp = client.list_objects_v2(
1547
- # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
1548
- # )
1549
-
1550
- # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
1551
-
1552
- # for category in categories:
1553
- # original_prefix = f"faceswap/target/{category}/original/"
1554
- # thumb_prefix = f"faceswap/target/{category}/thumb/"
1555
-
1556
- # original_objects = client.list_objects_v2(
1557
- # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
1558
- # ).get("Contents", [])
1559
-
1560
- # thumb_objects = client.list_objects_v2(
1561
- # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
1562
- # ).get("Contents", [])
1563
-
1564
- # original_filenames = sorted([
1565
- # obj["Key"].split("/")[-1] for obj in original_objects
1566
- # if obj["Key"].split("/")[-1].endswith(".png")
1567
- # ])
1568
-
1569
- # for idx, filename in enumerate(original_filenames, start=1):
1570
- # cid = f"{category.lower()}image_{idx}"
1571
- # if cid == target_category_id:
1572
- # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
1573
- # break
1574
-
1575
- # if target_url:
1576
- # break
1577
-
1578
- # if not target_url:
1579
- # raise HTTPException(404, "Target categoryId not found")
1580
-
1581
- # async with httpx.AsyncClient(timeout=30.0) as client:
1582
- # response = await client.get(target_url)
1583
- # response.raise_for_status()
1584
- # tgt_bytes = response.content
1585
-
1586
- # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
1587
- # if tgt_bgr is None:
1588
- # raise HTTPException(400, "Invalid target image data")
1589
-
1590
- # # -----------------------------
1591
- # # Merge all source faces
1592
- # # -----------------------------
1593
- # all_src_faces = []
1594
- # for img in src_images:
1595
- # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
1596
- # all_src_faces.extend(faces)
1597
-
1598
- # if not all_src_faces:
1599
- # raise HTTPException(400, "No faces detected in source images")
1600
-
1601
- # tgt_faces = face_analysis_app.get(tgt_bgr)
1602
- # if not tgt_faces:
1603
- # raise HTTPException(400, "No faces detected in target image")
1604
-
1605
- # # -----------------------------
1606
- # # Gender-based pairing
1607
- # # -----------------------------
1608
- # def face_sort_key(face):
1609
- # x1, y1, x2, y2 = face.bbox
1610
- # area = (x2 - x1) * (y2 - y1)
1611
- # cx = (x1 + x2) / 2
1612
- # return (-area, cx)
1613
-
1614
- # # Separate by gender
1615
- # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
1616
- # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
1617
- # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
1618
- # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
1619
-
1620
- # pairs = []
1621
- # for s, t in zip(src_male, tgt_male):
1622
- # pairs.append((s, t))
1623
- # for s, t in zip(src_female, tgt_female):
1624
- # pairs.append((s, t))
1625
-
1626
- # # fallback if gender mismatch
1627
- # if not pairs:
1628
- # src_all = sorted(all_src_faces, key=face_sort_key)
1629
- # tgt_all = sorted(tgt_faces, key=face_sort_key)
1630
- # pairs = list(zip(src_all, tgt_all))
1631
-
1632
- # # -----------------------------
1633
- # # Perform face swap
1634
- # # -----------------------------
1635
- # with swap_lock:
1636
- # result_img = tgt_bgr.copy()
1637
- # for src_face, _ in pairs:
1638
- # if face_analysis_app is None:
1639
- # raise HTTPException(status_code=500, detail="Face analysis models not initialized. Please ensure models are downloaded.")
1640
- # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
1641
- # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
1642
- # target_face = candidates[0]
1643
- # if swapper is None:
1644
- # raise HTTPException(status_code=500, detail="Face swap models not initialized. Please ensure models are downloaded.")
1645
- # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
1646
-
1647
- # result_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
1648
-
1649
- # # -----------------------------
1650
- # # Mandatory enhancement
1651
- # # -----------------------------
1652
- # enhanced_rgb = mandatory_enhancement(result_rgb)
1653
- # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
1654
-
1655
- # # -----------------------------
1656
- # # Save, upload, compress
1657
- # # -----------------------------
1658
- # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
1659
- # final_path = os.path.join(temp_dir, "result.png")
1660
- # cv2.imwrite(final_path, enhanced_bgr)
1661
-
1662
- # with open(final_path, "rb") as f:
1663
- # result_bytes = f.read()
1664
-
1665
- # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
1666
- # result_url = upload_to_spaces(result_bytes, result_key)
1667
-
1668
- # compressed_bytes = compress_image(result_bytes, max_size=(1280, 1280), quality=72)
1669
- # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
1670
- # compressed_url = upload_to_spaces(compressed_bytes, compressed_key, content_type="image/jpeg")
1671
-
1672
- # # -----------------------------
1673
- # # Log API usage
1674
- # # -----------------------------
1675
- # end_time = datetime.utcnow()
1676
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1677
- # if database is not None:
1678
- # await database.api_logs.insert_one({
1679
- # "endpoint": "/face-swap-couple",
1680
- # "status": "success",
1681
- # "response_time_ms": response_time_ms,
1682
- # "timestamp": end_time
1683
- # })
1684
-
1685
- # return {
1686
- # "result_key": result_key,
1687
- # "result_url": result_url,
1688
- # "compressed_url": compressed_url
1689
- # }
1690
-
1691
- # except Exception as e:
1692
- # end_time = datetime.utcnow()
1693
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1694
- # if database is not None:
1695
- # await database.api_logs.insert_one({
1696
- # "endpoint": "/face-swap-couple",
1697
- # "status": "fail",
1698
- # "response_time_ms": response_time_ms,
1699
- # "timestamp": end_time,
1700
- # "error": str(e)
1701
- # })
1702
- # raise HTTPException(500, f"Face swap failed: {str(e)}")
1703
-
1704
-
1705
-
1706
-
1707
- # # --------------------- Mount Gradio ---------------------
1708
-
1709
- # multi_faceswap_app = build_multi_faceswap_gradio()
1710
- # fastapi_app = mount_gradio_app(
1711
- # fastapi_app,
1712
- # multi_faceswap_app,
1713
- # path="/gradio-couple-faceswap"
1714
- # )
1715
-
1716
-
1717
-
1718
- # if __name__ == "__main__":
1719
- # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
1720
-
1721
-
1722
- # --------------------- List Images Endpoint ---------------------
1723
  # --------------------- List Images Endpoint ---------------------
1724
  import os
1725
  os.environ["OMP_NUM_THREADS"] = "1"
@@ -1799,7 +77,24 @@ if COLLAGE_MAKER_DB_URL:
1799
  collage_maker_db = collage_maker_client.adminPanel
1800
  collage_media_clicks_col = collage_maker_db.media_clicks
1801
  except Exception as e:
1802
- logger.warning(f"MongoDB collage-maker connection failed (optional): {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1803
 
1804
  # OLD logs DB
1805
  MONGODB_URL = os.getenv("MONGODB_URL")
@@ -1933,19 +228,40 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security))
1933
  return credentials.credentials
1934
 
1935
  # --------------------- DB Selector ---------------------
1936
- def get_media_clicks_collection(appname: Optional[str] = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
1937
  """
1938
- Returns the correct media_clicks collection based on appname.
1939
- Defaults to the primary admin database when no appname is provided
1940
- or when the requested database is unavailable.
1941
  """
1942
  if appname:
1943
- normalized = appname.strip().lower()
1944
- if normalized == "collage-maker":
1945
- if collage_media_clicks_col is not None:
1946
- return collage_media_clicks_col
1947
- logger.warning("COLLAGE_MAKER_DB_URL not configured; falling back to default media_clicks collection")
1948
- return media_clicks_col
 
 
 
 
 
 
 
 
 
1949
 
1950
  # --------------------- Logging API Hits ---------------------
1951
  async def log_faceswap_hit(token: str, status: str = "success"):
@@ -2255,7 +571,9 @@ async def face_swap_api(
2255
  if user_id == "":
2256
  user_id = None
2257
 
2258
- media_clicks_collection = get_media_clicks_collection(appname)
 
 
2259
 
2260
  logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
2261
 
@@ -2277,10 +595,14 @@ async def face_swap_api(
2277
  # ------------------------------------------------------------------
2278
  if new_category_id:
2279
 
2280
- doc = await subcategories_col.find_one({
 
 
 
2281
  "asset_images._id": ObjectId(new_category_id)
2282
  })
2283
 
 
2284
  if not doc:
2285
  raise HTTPException(404, "Asset image not found in database")
2286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # --------------------- List Images Endpoint ---------------------
2
  import os
3
  os.environ["OMP_NUM_THREADS"] = "1"
 
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 collage-maker 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")
 
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
  if appname:
250
+ app = appname.strip().lower()
251
+
252
+ if app == "collage-maker":
253
+ if collage_media_clicks_col and subcategories_col:
254
+ return collage_media_clicks_col, subcategories_col
255
+ logger.warning("Collage-maker DB not configured, falling back to admin")
256
+
257
+ elif app == "ai-enhancer":
258
+ if ai_enhancer_media_clicks_col and ai_enhancer_subcategories_col:
259
+ return ai_enhancer_media_clicks_col, ai_enhancer_subcategories_col
260
+ logger.warning("AI-Enhancer DB not configured, falling back to admin")
261
+
262
+ # default
263
+ return media_clicks_col, subcategories_col
264
+
265
 
266
  # --------------------- Logging API Hits ---------------------
267
  async def log_faceswap_hit(token: str, status: str = "success"):
 
571
  if user_id == "":
572
  user_id = None
573
 
574
+ # media_clicks_collection = get_media_clicks_collection(appname)
575
+ media_clicks_collection, subcategories_collection = get_app_db_collections(appname)
576
+
577
 
578
  logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
579
 
 
595
  # ------------------------------------------------------------------
596
  if new_category_id:
597
 
598
+ # doc = await subcategories_col.find_one({
599
+ # "asset_images._id": ObjectId(new_category_id)
600
+ # })
601
+ doc = await subcategories_collection.find_one({
602
  "asset_images._id": ObjectId(new_category_id)
603
  })
604
 
605
+
606
  if not doc:
607
  raise HTTPException(404, "Asset image not found in database")
608