LogicGoInfotechSpaces commited on
Commit
07ac4f2
·
verified ·
1 Parent(s): 13e7046

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -912
app.py CHANGED
@@ -324,25 +324,6 @@ def compress_image(
324
 
325
  return output.getvalue()
326
 
327
- # --------------------- Gradio ---------------------
328
- # with gr.Blocks() as demo:
329
- # gr.Markdown("Face Swap")
330
-
331
- # with gr.Row():
332
- # src_input = gr.Image(type="numpy", label="Upload Your Face")
333
- # tgt_input = gr.Image(type="numpy", label="Upload Target Image")
334
-
335
- # btn = gr.Button("Swap Face")
336
- # output_img = gr.Image(type="numpy", label="Enhanced Output")
337
- # download = gr.File(label="⬇️ Download Enhanced Image")
338
- # error_box = gr.Textbox(label="Logs / Errors", interactive=False)
339
-
340
- # def process(src, tgt):
341
- # img, path, err = face_swap_and_enhance(src, tgt)
342
- # return img, path, err
343
-
344
- # btn.click(process, [src_input, tgt_input], [output_img, download, error_box])
345
-
346
  # --------------------- DigitalOcean Spaces Helper ---------------------
347
  def get_spaces_client():
348
  session = boto3.session.Session()
@@ -389,6 +370,17 @@ def build_multi_faceswap_gradio():
389
  btn.click(process, [src, tgt], [out, error])
390
 
391
  return demo
 
 
 
 
 
 
 
 
 
 
 
392
 
393
  # --------------------- API Endpoints ---------------------
394
  @fastapi_app.get("/")
@@ -787,912 +779,78 @@ async def preview_result(result_key: str):
787
  media_type="image/png",
788
  headers={"Content-Disposition": "inline; filename=result.png"}
789
  )
790
- # --------------------- Mount Gradio ---------------------
791
-
792
- multi_faceswap_app = build_multi_faceswap_gradio()
793
- fastapi_app = mount_gradio_app(
794
- fastapi_app,
795
- multi_faceswap_app,
796
- path="/gradio-multi"
797
- )
798
-
799
- if __name__ == "__main__":
800
- uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
801
-
802
- # # --------------------- List Images Endpoint ---------------------
803
- # import os
804
- # os.environ["OMP_NUM_THREADS"] = "1"
805
- # import shutil
806
- # import uuid
807
- # import cv2
808
- # import numpy as np
809
- # import threading
810
- # import subprocess
811
- # import logging
812
- # import tempfile
813
- # import sys
814
- # from datetime import datetime,timedelta
815
-
816
- # import insightface
817
- # from insightface.app import FaceAnalysis
818
- # from huggingface_hub import hf_hub_download
819
- # from fastapi import FastAPI, UploadFile, File, HTTPException, Response, Depends, Security, Form
820
- # from fastapi.responses import RedirectResponse
821
- # from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
822
- # from motor.motor_asyncio import AsyncIOMotorClient
823
- # from bson import ObjectId
824
- # from bson.errors import InvalidId
825
- # import httpx
826
- # import uvicorn
827
- # import gradio as gr
828
- # from gradio import mount_gradio_app
829
- # from PIL import Image
830
- # import io
831
-
832
- # # DigitalOcean Spaces
833
- # import boto3
834
- # from botocore.client import Config
835
- # from typing import Optional
836
-
837
- # # --------------------- Logging ---------------------
838
- # logging.basicConfig(level=logging.INFO)
839
- # logger = logging.getLogger(__name__)
840
-
841
- # # --------------------- Secrets & Paths ---------------------
842
- # REPO_ID = "HariLogicgo/face_swap_models"
843
- # MODELS_DIR = "./models"
844
- # os.makedirs(MODELS_DIR, exist_ok=True)
845
-
846
- # HF_TOKEN = os.getenv("HF_TOKEN")
847
- # API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN")
848
-
849
- # DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
850
- # DO_SPACES_ENDPOINT = f"https://{DO_SPACES_REGION}.digitaloceanspaces.com"
851
- # DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
852
- # DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
853
- # DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
854
-
855
- # # NEW admin DB
856
- # ADMIN_MONGO_URL = os.getenv("ADMIN_MONGO_URL")
857
- # admin_client = AsyncIOMotorClient(ADMIN_MONGO_URL)
858
- # admin_db = admin_client.adminPanel
859
- # subcategories_col = admin_db.subcategories
860
- # media_clicks_col = admin_db.media_clicks
861
-
862
- # # OLD logs DB
863
- # MONGODB_URL = os.getenv("MONGODB_URL")
864
- # client = None
865
- # database = None
866
-
867
- # # --------------------- Download Models ---------------------
868
- # def download_models():
869
- # logger.info("Downloading models...")
870
- # inswapper_path = hf_hub_download(
871
- # repo_id=REPO_ID,
872
- # filename="models/inswapper_128.onnx",
873
- # repo_type="model",
874
- # local_dir=MODELS_DIR,
875
- # token=HF_TOKEN
876
- # )
877
-
878
- # buffalo_files = ["1k3d68.onnx", "2d106det.onnx", "genderage.onnx", "det_10g.onnx", "w600k_r50.onnx"]
879
- # for f in buffalo_files:
880
- # hf_hub_download(
881
- # repo_id=REPO_ID,
882
- # filename=f"models/buffalo_l/" + f,
883
- # repo_type="model",
884
- # local_dir=MODELS_DIR,
885
- # token=HF_TOKEN
886
- # )
887
-
888
- # logger.info("Models downloaded.")
889
- # return inswapper_path
890
-
891
-
892
- # inswapper_path = download_models()
893
-
894
- # # --------------------- Face Analysis + Swapper ---------------------
895
- # providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
896
- # face_analysis_app = FaceAnalysis(name="buffalo_l", root=MODELS_DIR, providers=providers)
897
- # face_analysis_app.prepare(ctx_id=0, det_size=(640, 640))
898
- # swapper = insightface.model_zoo.get_model(inswapper_path, providers=providers)
899
-
900
- # # --------------------- CodeFormer ---------------------
901
- # CODEFORMER_PATH = "CodeFormer/inference_codeformer.py"
902
-
903
- # def ensure_codeformer():
904
- # if not os.path.exists("CodeFormer"):
905
- # subprocess.run("git clone https://github.com/sczhou/CodeFormer.git", shell=True, check=True)
906
- # subprocess.run("pip install -r CodeFormer/requirements.txt", shell=True, check=True)
907
- # subprocess.run("python CodeFormer/basicsr/setup.py develop", shell=True, check=True)
908
- # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py facelib", shell=True, check=True)
909
- # subprocess.run("python CodeFormer/scripts/download_pretrained_models.py CodeFormer", shell=True, check=True)
910
-
911
- # ensure_codeformer()
912
-
913
-
914
- # # --------------------- FastAPI ---------------------
915
- # fastapi_app = FastAPI()
916
-
917
- # @fastapi_app.on_event("startup")
918
- # async def startup_db():
919
- # global client, database
920
- # logger.info("Initializing MongoDB for API logs...")
921
- # client = AsyncIOMotorClient(MONGODB_URL)
922
- # database = client.FaceSwap
923
- # logger.info("MongoDB initialized for API logs")
924
-
925
- # @fastapi_app.on_event("shutdown")
926
- # async def shutdown_db():
927
- # global client
928
- # if client:
929
- # client.close()
930
- # logger.info("MongoDB connection closed")
931
-
932
- # # --------------------- Auth ---------------------
933
- # security = HTTPBearer()
934
-
935
- # def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
936
- # if credentials.credentials != API_SECRET_TOKEN:
937
- # raise HTTPException(status_code=401, detail="Invalid or missing token")
938
- # return credentials.credentials
939
-
940
- # # --------------------- Logging API Hits ---------------------
941
- # async def log_faceswap_hit(token: str, status: str = "success"):
942
- # global database
943
- # if database is None:
944
- # return
945
- # await database.api_logs.insert_one({
946
- # "token": token,
947
- # "endpoint": "/faceswap",
948
- # "status": status,
949
- # "timestamp": datetime.utcnow()
950
- # })
951
-
952
- # # --------------------- Face Swap Pipeline ---------------------
953
- # swap_lock = threading.Lock()
954
-
955
- # def enhance_image_with_codeformer(rgb_img, temp_dir=None):
956
- # if temp_dir is None:
957
- # temp_dir = os.path.join(tempfile.gettempdir(), f"enhance_{uuid.uuid4().hex[:8]}")
958
- # os.makedirs(temp_dir, exist_ok=True)
959
-
960
- # input_path = os.path.join(temp_dir, "input.jpg")
961
- # cv2.imwrite(input_path, cv2.cvtColor(rgb_img, cv2.COLOR_RGB2BGR))
962
-
963
- # python_cmd = sys.executable if sys.executable else "python3"
964
- # cmd = (
965
- # f"{python_cmd} {CODEFORMER_PATH} "
966
- # f"-w 0.4 "
967
- # f"--input_path {input_path} "
968
- # f"--output_path {temp_dir} "
969
- # f"--bg_upsampler realesrgan "
970
- # f"--face_upsample"
971
- # )
972
-
973
- # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
974
- # if result.returncode != 0:
975
- # raise RuntimeError(result.stderr)
976
-
977
- # final_dir = os.path.join(temp_dir, "final_results")
978
- # files = [f for f in os.listdir(final_dir) if f.endswith(".png")]
979
- # if not files:
980
- # raise RuntimeError("No enhanced output")
981
-
982
- # final_path = os.path.join(final_dir, files[0])
983
- # enhanced = cv2.imread(final_path)
984
- # return cv2.cvtColor(enhanced, cv2.COLOR_BGR2RGB)
985
-
986
- # import numpy as np
987
- # from sklearn.metrics.pairwise import cosine_similarity
988
-
989
- # SIM_THRESHOLD = 0.35 # strict
990
- # MAX_YAW = 35
991
- # MAX_PITCH = 25
992
- # MIN_FACE_AREA = 10000
993
-
994
-
995
- # def face_ok(face):
996
- # yaw, pitch, roll = face.pose
997
- # x1, y1, x2, y2 = face.bbox
998
- # area = (x2 - x1) * (y2 - y1)
999
-
1000
- # return (
1001
- # abs(yaw) <= MAX_YAW and
1002
- # abs(pitch) <= MAX_PITCH and
1003
- # area >= MIN_FACE_AREA
1004
- # )
1005
-
1006
-
1007
- # def quality_score(face):
1008
- # yaw, pitch, roll = face.pose
1009
- # x1, y1, x2, y2 = face.bbox
1010
- # area = (x2 - x1) * (y2 - y1)
1011
- # return area - abs(yaw)*200 - abs(pitch)*150
1012
-
1013
-
1014
- # def multi_face_swap_industry(src_img, tgt_img):
1015
- # """
1016
- # Industry-grade 2-face swap
1017
- # Returns swapped RGB image or raises error
1018
- # """
1019
-
1020
- # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
1021
- # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
1022
-
1023
- # src_faces = face_analysis_app.get(src_bgr)
1024
- # tgt_faces = face_analysis_app.get(tgt_bgr)
1025
-
1026
- # if len(src_faces) < 1 or len(tgt_faces) < 1:
1027
- # raise ValueError("No faces detected")
1028
-
1029
- # if len(src_faces) > 2 or len(tgt_faces) > 2:
1030
- # raise ValueError("More than 2 faces detected (blocked)")
1031
-
1032
- # # ---- Quality filtering ----
1033
- # src_faces = [f for f in src_faces if face_ok(f)]
1034
- # tgt_faces = [f for f in tgt_faces if face_ok(f)]
1035
-
1036
- # if not src_faces or not tgt_faces:
1037
- # raise ValueError("Faces rejected due to pose/quality")
1038
-
1039
- # # ---- Sort by quality (best first) ----
1040
- # src_faces = sorted(src_faces, key=quality_score, reverse=True)
1041
- # tgt_faces = sorted(tgt_faces, key=quality_score, reverse=True)
1042
-
1043
- # # ---- Extract embeddings ----
1044
- # src_embs = np.array([f.normed_embedding for f in src_faces])
1045
- # tgt_embs = np.array([f.normed_embedding for f in tgt_faces])
1046
-
1047
- # sim = cosine_similarity(src_embs, tgt_embs)
1048
-
1049
- # used_tgts = set()
1050
- # pairs = []
1051
-
1052
- # for i in range(len(src_faces)):
1053
- # j = np.argmax(sim[i])
1054
- # score = sim[i][j]
1055
-
1056
- # if score < SIM_THRESHOLD:
1057
- # raise ValueError(f"Low identity similarity ({score:.2f})")
1058
-
1059
- # if j in used_tgts:
1060
- # continue
1061
-
1062
- # pairs.append((src_faces[i], tgt_faces[j], score))
1063
- # used_tgts.add(j)
1064
-
1065
- # if not pairs:
1066
- # raise ValueError("No valid face pairs")
1067
-
1068
- # # ---- Swap WITHOUT re-detection ----
1069
- # result = tgt_bgr.copy()
1070
-
1071
- # for src_face, tgt_face, score in pairs:
1072
- # result = swapper.get(
1073
- # result,
1074
- # tgt_face,
1075
- # src_face,
1076
- # paste_back=True
1077
- # )
1078
-
1079
- # return cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
1080
 
1081
- # # def multi_face_swap(src_img, tgt_img):
1082
- # # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
1083
- # # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
1084
-
1085
- # # src_faces = face_analysis_app.get(src_bgr)
1086
- # # tgt_faces = face_analysis_app.get(tgt_bgr)
1087
-
1088
- # # if not src_faces or not tgt_faces:
1089
- # # raise ValueError("No faces detected")
1090
-
1091
- # # def face_sort_key(face):
1092
- # # x1, y1, x2, y2 = face.bbox
1093
- # # area = (x2 - x1) * (y2 - y1)
1094
- # # cx = (x1 + x2) / 2
1095
- # # return (-area, cx)
1096
-
1097
- # # # Split by gender
1098
- # # src_male = [f for f in src_faces if f.gender == 1]
1099
- # # src_female = [f for f in src_faces if f.gender == 0]
1100
-
1101
- # # tgt_male = [f for f in tgt_faces if f.gender == 1]
1102
- # # tgt_female = [f for f in tgt_faces if f.gender == 0]
1103
-
1104
- # # # Sort inside gender groups
1105
- # # src_male = sorted(src_male, key=face_sort_key)
1106
- # # src_female = sorted(src_female, key=face_sort_key)
1107
-
1108
- # # tgt_male = sorted(tgt_male, key=face_sort_key)
1109
- # # tgt_female = sorted(tgt_female, key=face_sort_key)
1110
-
1111
- # # # Build final swap pairs
1112
- # # pairs = []
1113
-
1114
- # # for s, t in zip(src_male, tgt_male):
1115
- # # pairs.append((s, t))
1116
-
1117
- # # for s, t in zip(src_female, tgt_female):
1118
- # # pairs.append((s, t))
1119
-
1120
- # # # Fallback if gender mismatch
1121
- # # if not pairs:
1122
- # # src_faces = sorted(src_faces, key=face_sort_key)
1123
- # # tgt_faces = sorted(tgt_faces, key=face_sort_key)
1124
- # # pairs = list(zip(src_faces, tgt_faces))
1125
-
1126
- # # result_img = tgt_bgr.copy()
1127
-
1128
- # # for src_face, _ in pairs:
1129
- # # # 🔁 re-detect current target faces
1130
- # # current_faces = face_analysis_app.get(result_img)
1131
- # # current_faces = sorted(current_faces, key=face_sort_key)
1132
-
1133
- # # # choose best matching gender
1134
- # # candidates = [
1135
- # # f for f in current_faces if f.gender == src_face.gender
1136
- # # ] or current_faces
1137
-
1138
- # # target_face = candidates[0]
1139
-
1140
- # # result_img = swapper.get(
1141
- # # result_img,
1142
- # # target_face,
1143
- # # src_face,
1144
- # # paste_back=True
1145
- # # )
1146
-
1147
- # # return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
1148
-
1149
-
1150
-
1151
- # def face_swap_and_enhance(src_img, tgt_img, temp_dir=None):
1152
- # try:
1153
- # with swap_lock:
1154
- # # Use a temp dir for intermediate files
1155
- # if temp_dir is None:
1156
- # temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_work_{uuid.uuid4().hex[:8]}")
1157
- # if os.path.exists(temp_dir):
1158
- # shutil.rmtree(temp_dir)
1159
- # os.makedirs(temp_dir, exist_ok=True)
1160
-
1161
- # src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
1162
- # tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
1163
-
1164
- # src_faces = face_analysis_app.get(src_bgr)
1165
- # tgt_faces = face_analysis_app.get(tgt_bgr)
1166
- # if not src_faces or not tgt_faces:
1167
- # return None, None, "❌ Face not detected in one of the images"
1168
-
1169
- # swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
1170
- # swapped_rgb = multi_face_swap_industry(src_img, tgt_img)
1171
- # swapped_bgr = cv2.cvtColor(swapped_rgb, cv2.COLOR_RGB2BGR)
1172
-
1173
- # if swapped_bgr is None:
1174
- # return None, None, "❌ Face swap failed"
1175
-
1176
- # cv2.imwrite(swapped_path, swapped_bgr)
1177
-
1178
- # python_cmd = sys.executable if sys.executable else "python3"
1179
- # cmd = f"{python_cmd} {CODEFORMER_PATH} -w 0.4 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
1180
- # result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
1181
- # if result.returncode != 0:
1182
- # return None, None, f"❌ CodeFormer failed:\n{result.stderr}"
1183
-
1184
- # final_results_dir = os.path.join(temp_dir, "final_results")
1185
- # final_files = [f for f in os.listdir(final_results_dir) if f.endswith(".png")]
1186
- # if not final_files:
1187
- # return None, None, "❌ No enhanced image found"
1188
-
1189
- # final_path = os.path.join(final_results_dir, final_files[0])
1190
- # final_img_bgr = cv2.imread(final_path)
1191
- # if final_img_bgr is None:
1192
- # return None, None, "❌ Failed to read enhanced image file"
1193
- # final_img = cv2.cvtColor(final_img_bgr, cv2.COLOR_BGR2RGB)
1194
-
1195
- # return final_img, final_path, ""
1196
-
1197
- # except Exception as e:
1198
- # return None, None, f"❌ Error: {str(e)}"
1199
-
1200
- # def compress_image(
1201
- # image_bytes: bytes,
1202
- # max_size=(1280, 1280), # max width/height
1203
- # quality=75 # JPEG quality (60–80 is ideal)
1204
- # ) -> bytes:
1205
- # """
1206
- # Compress image by resizing and lowering quality.
1207
- # Returns compressed image bytes.
1208
- # """
1209
- # img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
1210
-
1211
- # # Resize while maintaining aspect ratio
1212
- # img.thumbnail(max_size, Image.LANCZOS)
1213
-
1214
- # output = io.BytesIO()
1215
- # img.save(
1216
- # output,
1217
- # format="JPEG",
1218
- # quality=quality,
1219
- # optimize=True,
1220
- # progressive=True
1221
- # )
1222
-
1223
- # return output.getvalue()
1224
-
1225
- # # --------------------- Gradio ---------------------
1226
- # # with gr.Blocks() as demo:
1227
- # # gr.Markdown("Face Swap")
1228
-
1229
- # # with gr.Row():
1230
- # # src_input = gr.Image(type="numpy", label="Upload Your Face")
1231
- # # tgt_input = gr.Image(type="numpy", label="Upload Target Image")
1232
-
1233
- # # btn = gr.Button("Swap Face")
1234
- # # output_img = gr.Image(type="numpy", label="Enhanced Output")
1235
- # # download = gr.File(label="⬇️ Download Enhanced Image")
1236
- # # error_box = gr.Textbox(label="Logs / Errors", interactive=False)
1237
-
1238
- # # def process(src, tgt):
1239
- # # img, path, err = face_swap_and_enhance(src, tgt)
1240
- # # return img, path, err
1241
-
1242
- # # btn.click(process, [src_input, tgt_input], [output_img, download, error_box])
1243
-
1244
- # # --------------------- DigitalOcean Spaces Helper ---------------------
1245
- # def get_spaces_client():
1246
- # session = boto3.session.Session()
1247
- # client = session.client(
1248
- # 's3',
1249
- # region_name=DO_SPACES_REGION,
1250
- # endpoint_url=DO_SPACES_ENDPOINT,
1251
- # aws_access_key_id=DO_SPACES_KEY,
1252
- # aws_secret_access_key=DO_SPACES_SECRET,
1253
- # config=Config(signature_version='s3v4')
1254
- # )
1255
- # return client
1256
-
1257
- # def upload_to_spaces(file_bytes, key, content_type="image/png"):
1258
- # client = get_spaces_client()
1259
- # client.put_object(Bucket=DO_SPACES_BUCKET, Key=key, Body=file_bytes, ContentType=content_type, ACL='public-read')
1260
- # return f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
1261
-
1262
- # def download_from_spaces(key):
1263
- # client = get_spaces_client()
1264
- # obj = client.get_object(Bucket=DO_SPACES_BUCKET, Key=key)
1265
- # return obj['Body'].read()
1266
-
1267
- # def build_multi_faceswap_gradio():
1268
- # with gr.Blocks() as demo:
1269
- # gr.Markdown("## 👩‍❤️‍👨 Multi Face Swap (Couple → Couple)")
1270
-
1271
- # with gr.Row():
1272
- # src = gr.Image(type="numpy", label="Source Image (2 Faces)")
1273
- # tgt = gr.Image(type="numpy", label="Target Image (2 Faces)")
1274
-
1275
- # out = gr.Image(type="numpy", label="Swapped Result")
1276
- # error = gr.Textbox(label="Logs", interactive=False)
1277
-
1278
- # def process(src_img, tgt_img):
1279
- # try:
1280
- # swapped = multi_face_swap_industry(src_img, tgt_img)
1281
- # enhanced = enhance_image_with_codeformer(swapped)
1282
- # return enhanced, ""
1283
- # except Exception as e:
1284
- # return None, str(e)
1285
-
1286
- # btn = gr.Button("Swap Faces")
1287
- # btn.click(process, [src, tgt], [out, error])
1288
-
1289
- # return demo
1290
-
1291
- # # --------------------- API Endpoints ---------------------
1292
- # @fastapi_app.get("/")
1293
- # def root():
1294
- # return {"status": "healthy"}
1295
-
1296
- # @fastapi_app.get("/health")
1297
- # async def health():
1298
- # return {"status": "healthy"}
1299
-
1300
- # from fastapi import Form
1301
- # import requests
1302
- # @fastapi_app.get("/test-admin-db")
1303
- # async def test_admin_db():
1304
- # try:
1305
- # doc = await admin_db.list_collection_names()
1306
- # return {"ok": True, "collections": doc}
1307
- # except Exception as e:
1308
- # return {"ok": False, "error": str(e), "url": ADMIN_MONGO_URL}
1309
-
1310
- # @fastapi_app.post("/face-swap", dependencies=[Depends(verify_token)])
1311
- # async def face_swap_api(
1312
- # source: UploadFile = File(...),
1313
- # target_category_id: str = Form(None),
1314
- # new_category_id: str = Form(None),
1315
- # user_id: Optional[str] = Form(None),
1316
- # credentials: HTTPAuthorizationCredentials = Security(security)
1317
- # ):
1318
- # start_time = datetime.utcnow()
1319
-
1320
- # try:
1321
- # # ------------------------------------------------------------------
1322
- # # VALIDATION
1323
- # # ------------------------------------------------------------------
1324
- # # --------------------------------------------------------------
1325
- # # BACKWARD COMPATIBILITY FOR OLD ANDROID VERSIONS
1326
- # # --------------------------------------------------------------
1327
- # if target_category_id == "":
1328
- # target_category_id = None
1329
-
1330
- # if new_category_id == "":
1331
- # new_category_id = None
1332
-
1333
- # if user_id == "":
1334
- # user_id = None
1335
-
1336
- # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
1337
-
1338
- # if target_category_id and new_category_id:
1339
- # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
1340
 
1341
- # if not target_category_id and not new_category_id:
1342
- # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
 
 
 
 
1343
 
1344
- # # ------------------------------------------------------------------
1345
- # # READ SOURCE IMAGE
1346
- # # ------------------------------------------------------------------
1347
- # src_bytes = await source.read()
1348
- # src_key = f"faceswap/source/{uuid.uuid4().hex}_{source.filename}"
1349
- # upload_to_spaces(src_bytes, src_key, content_type=source.content_type)
1350
 
1351
- # # ------------------------------------------------------------------
1352
- # # CASE 1 : new_category_id → MongoDB lookup
1353
- # # ------------------------------------------------------------------
1354
- # if new_category_id:
1355
 
1356
- # doc = await subcategories_col.find_one({
1357
- # "asset_images._id": ObjectId(new_category_id)
1358
- # })
1359
 
1360
- # if not doc:
1361
- # raise HTTPException(404, "Asset image not found in database")
 
 
1362
 
1363
- # # extract correct asset
1364
- # asset = next(
1365
- # (img for img in doc["asset_images"] if str(img["_id"]) == new_category_id),
1366
- # None
1367
- # )
1368
 
1369
- # if not asset:
1370
- # raise HTTPException(404, "Asset image URL not found")
1371
 
1372
- # # correct URL
1373
- # target_url = asset["url"]
 
 
 
 
1374
 
1375
- # # correct categoryId (ObjectId)
1376
- # #category_oid = doc["categoryId"] # <-- DO NOT CONVERT TO STRING
1377
- # subcategory_oid = doc["_id"]
1378
-
1379
- # # ------------------------------------------------------------------#
1380
- # # # MEDIA_CLICKS (ONLY IF user_id PRESENT)
1381
- # # ------------------------------------------------------------------#
1382
- # if user_id:
1383
- # try:
1384
- # user_id_clean = user_id.strip()
1385
- # if not user_id_clean:
1386
- # raise ValueError("user_id cannot be empty")
1387
- # try:
1388
- # user_oid = ObjectId(user_id_clean)
1389
- # except (InvalidId, ValueError) as e:
1390
- # logger.error(f"Invalid user_id format: {user_id_clean}")
1391
- # raise ValueError(f"Invalid user_id format: {user_id_clean}")
1392
-
1393
- # now = datetime.utcnow()
1394
-
1395
- # # Normalize dates (UTC midnight)
1396
- # today_date = datetime(now.year, now.month, now.day)
1397
-
1398
- # # -------------------------------------------------
1399
- # # STEP 1: Ensure root document exists
1400
- # # -------------------------------------------------
1401
- # await media_clicks_col.update_one(
1402
- # {"userId": user_oid},
1403
- # {
1404
- # "$setOnInsert": {
1405
- # "userId": user_oid,
1406
- # "createdAt": now,
1407
- # "ai_edit_complete": 0,
1408
- # "ai_edit_daily_count": []
1409
- # }
1410
- # },
1411
- # upsert=True
1412
- # )
1413
- # # -------------------------------------------------
1414
- # # STEP 2: Handle DAILY USAGE (BINARY, NO DUPLICATES)
1415
- # # -------------------------------------------------
1416
- # doc = await media_clicks_col.find_one(
1417
- # {"userId": user_oid},
1418
- # {"ai_edit_daily_count": 1}
1419
- # )
1420
-
1421
- # daily_entries = doc.get("ai_edit_daily_count", []) if doc else []
1422
-
1423
- # # Normalize today to UTC midnight
1424
- # today_date = datetime(now.year, now.month, now.day)
1425
-
1426
- # # Build normalized date → count map (THIS ENFORCES UNIQUENESS)
1427
- # daily_map = {}
1428
- # for entry in daily_entries:
1429
- # d = entry["date"]
1430
- # if isinstance(d, datetime):
1431
- # d = datetime(d.year, d.month, d.day)
1432
- # daily_map[d] = entry["count"] # overwrite = no duplicates
1433
-
1434
- # # Determine last recorded date
1435
- # last_date = max(daily_map.keys()) if daily_map else today_date
1436
-
1437
- # # Fill ALL missing days with count = 0
1438
- # next_day = last_date + timedelta(days=1)
1439
- # while next_day < today_date:
1440
- # daily_map.setdefault(next_day, 0)
1441
- # next_day += timedelta(days=1)
1442
-
1443
- # # Mark today as used (binary)
1444
- # daily_map[today_date] = 1
1445
-
1446
- # # Rebuild list: OLDEST → NEWEST
1447
- # final_daily_entries = [
1448
- # {"date": d, "count": daily_map[d]}
1449
- # for d in sorted(daily_map.keys())
1450
- # ]
1451
-
1452
- # # Keep only last 32 days
1453
- # final_daily_entries = final_daily_entries[-32:]
1454
-
1455
- # # Atomic replace
1456
- # await media_clicks_col.update_one(
1457
- # {"userId": user_oid},
1458
- # {
1459
- # "$set": {
1460
- # "ai_edit_daily_count": final_daily_entries,
1461
- # "updatedAt": now
1462
- # }
1463
- # }
1464
- # )
1465
-
1466
- # # -------------------------------------------------
1467
- # # STEP 3: Try updating existing subCategory
1468
- # # -------------------------------------------------
1469
- # update_result = await media_clicks_col.update_one(
1470
- # {
1471
- # "userId": user_oid,
1472
- # "subCategories.subCategoryId": subcategory_oid
1473
- # },
1474
- # {
1475
- # "$inc": {
1476
- # "subCategories.$.click_count": 1,
1477
- # "ai_edit_complete": 1
1478
- # },
1479
- # "$set": {
1480
- # "subCategories.$.lastClickedAt": now,
1481
- # "ai_edit_last_date": now,
1482
- # "updatedAt": now
1483
- # }
1484
- # }
1485
- # )
1486
-
1487
- # # -------------------------------------------------
1488
- # # STEP 4: Push subCategory if missing
1489
- # # -------------------------------------------------
1490
- # if update_result.matched_count == 0:
1491
- # await media_clicks_col.update_one(
1492
- # {"userId": user_oid},
1493
- # {
1494
- # "$inc": {
1495
- # "ai_edit_complete": 1
1496
- # },
1497
- # "$set": {
1498
- # "ai_edit_last_date": now,
1499
- # "updatedAt": now
1500
- # },
1501
- # "$push": {
1502
- # "subCategories": {
1503
- # "subCategoryId": subcategory_oid,
1504
- # "click_count": 1,
1505
- # "lastClickedAt": now
1506
- # }
1507
- # }
1508
- # }
1509
- # )
1510
-
1511
- # # -------------------------------------------------
1512
- # # STEP 5: Sort subCategories by lastClickedAt (ascending - oldest first)
1513
- # # -------------------------------------------------
1514
- # user_doc = await media_clicks_col.find_one({"userId": user_oid})
1515
- # if user_doc and "subCategories" in user_doc:
1516
- # subcategories = user_doc["subCategories"]
1517
- # # Sort by lastClickedAt in ascending order (oldest first)
1518
- # # Handle missing or None dates by using datetime.min
1519
- # subcategories_sorted = sorted(
1520
- # subcategories,
1521
- # key=lambda x: x.get("lastClickedAt") if x.get("lastClickedAt") is not None else datetime.min
1522
- # )
1523
- # # Update with sorted array
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
- # # # ------------------------------------------------------------------
1544
- # # # CASE 2 : target_category_id → DigitalOcean path (unchanged logic)
1545
- # # # ------------------------------------------------------------------
1546
- # if target_category_id:
1547
- # client = get_spaces_client()
1548
- # base_prefix = "faceswap/target/"
1549
- # resp = client.list_objects_v2(
1550
- # Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/"
1551
- # )
1552
-
1553
- # # Extract categories from the CommonPrefixes
1554
- # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
1555
 
1556
- # target_url = None
1557
-
1558
- # # --- FIX STARTS HERE ---
1559
- # for category in categories:
1560
- # original_prefix = f"faceswap/target/{category}/original/"
1561
- # thumb_prefix = f"faceswap/target/{category}/thumb/" # Keep for file list check (optional but safe)
1562
-
1563
- # # List objects in original/
1564
- # original_objects = client.list_objects_v2(
1565
- # Bucket=DO_SPACES_BUCKET, Prefix=original_prefix
1566
- # ).get("Contents", [])
1567
-
1568
- # # List objects in thumb/ (optional: for the old code's extra check)
1569
- # thumb_objects = client.list_objects_v2(
1570
- # Bucket=DO_SPACES_BUCKET, Prefix=thumb_prefix
1571
- # ).get("Contents", [])
1572
-
1573
- # # Extract only the filenames and filter for .png
1574
- # original_filenames = sorted([
1575
- # obj["Key"].split("/")[-1] for obj in original_objects
1576
- # if obj["Key"].split("/")[-1].endswith(".png")
1577
- # ])
1578
- # thumb_filenames = [
1579
- # obj["Key"].split("/")[-1] for obj in thumb_objects
1580
- # ]
1581
-
1582
- # # Replicate the old indexing logic based on sorted filenames
1583
- # for idx, filename in enumerate(original_filenames, start=1):
1584
- # cid = f"{category.lower()}image_{idx}"
1585
-
1586
- # # Optional: Replicate the thumb file check for 100% parity
1587
- # # if filename in thumb_filenames and cid == target_category_id:
1588
- # # Simpler check just on the ID, assuming thumb files are present
1589
- # if cid == target_category_id:
1590
- # # Construct the final target URL using the full prefix and the filename
1591
- # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
1592
- # break
1593
-
1594
- # if target_url:
1595
- # break
1596
- # # --- FIX ENDS HERE ---
1597
 
1598
- # if not target_url:
1599
- # raise HTTPException(404, "Target categoryId not found")
1600
- # # # ------------------------------------------------------------------
1601
- # # # DOWNLOAD TARGET IMAGE
1602
- # # # ------------------------------------------------------------------
1603
- # async with httpx.AsyncClient(timeout=30.0) as client:
1604
- # response = await client.get(target_url)
1605
- # response.raise_for_status()
1606
- # tgt_bytes = response.content
1607
 
1608
- # src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
1609
- # tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
1610
 
1611
- # if src_bgr is None or tgt_bgr is None:
1612
- # raise HTTPException(400, "Invalid image data")
1613
 
1614
- # src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
1615
- # tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
 
 
 
 
1616
 
1617
- # # ------------------------------------------------------------------
1618
- # # FACE SWAP EXECUTION
1619
- # # ------------------------------------------------------------------
1620
- # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
1621
- # if err:
1622
- # raise HTTPException(500, err)
1623
-
1624
- # with open(final_path, "rb") as f:
1625
- # result_bytes = f.read()
1626
-
1627
- # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
1628
- # result_url = upload_to_spaces(result_bytes, result_key)
1629
- # # -------------------------------------------------
1630
- # # COMPRESS IMAGE (2–3 MB target)
1631
- # # -------------------------------------------------
1632
- # compressed_bytes = compress_image(
1633
- # image_bytes=result_bytes,
1634
- # max_size=(1280, 1280),
1635
- # quality=72
1636
- # )
1637
-
1638
- # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
1639
- # compressed_url = upload_to_spaces(
1640
- # compressed_bytes,
1641
- # compressed_key,
1642
- # content_type="image/jpeg"
1643
- # )
1644
- # end_time = datetime.utcnow()
1645
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1646
-
1647
- # if database is not None:
1648
- # await database.api_logs.insert_one({
1649
- # "endpoint": "/face-swap",
1650
- # "status": "success",
1651
- # "response_time_ms": response_time_ms,
1652
- # "timestamp": end_time
1653
- # })
1654
-
1655
-
1656
- # return {
1657
- # "result_key": result_key,
1658
- # "result_url": result_url,
1659
- # "Compressed_Image_URL": compressed_url
1660
- # }
1661
-
1662
- # except Exception as e:
1663
- # end_time = datetime.utcnow()
1664
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1665
-
1666
- # if database is not None:
1667
- # await database.api_logs.insert_one({
1668
- # "endpoint": "/face-swap",
1669
- # "status": "fail",
1670
- # "response_time_ms": response_time_ms,
1671
- # "timestamp": end_time,
1672
- # "error": str(e)
1673
- # })
1674
-
1675
- # raise HTTPException(500, f"Face swap failed: {str(e)}")
1676
-
1677
- # @fastapi_app.get("/preview/{result_key:path}")
1678
- # async def preview_result(result_key: str):
1679
- # try:
1680
- # img_bytes = download_from_spaces(result_key)
1681
- # except Exception:
1682
- # raise HTTPException(status_code=404, detail="Result not found")
1683
- # return Response(
1684
- # content=img_bytes,
1685
- # media_type="image/png",
1686
- # headers={"Content-Disposition": "inline; filename=result.png"}
1687
- # )
1688
- # # --------------------- Mount Gradio ---------------------
1689
-
1690
- # multi_faceswap_app = build_multi_faceswap_gradio()
1691
- # fastapi_app = mount_gradio_app(
1692
- # fastapi_app,
1693
- # multi_faceswap_app,
1694
- # path="/gradio-multi"
1695
- # )
1696
-
1697
- # if __name__ == "__main__":
1698
- # uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
 
324
 
325
  return output.getvalue()
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  # --------------------- DigitalOcean Spaces Helper ---------------------
328
  def get_spaces_client():
329
  session = boto3.session.Session()
 
370
  btn.click(process, [src, tgt], [out, error])
371
 
372
  return demo
373
+
374
+ def mandatory_enhancement(rgb_img):
375
+ """
376
+ Always runs CodeFormer on the final image.
377
+ Fail-safe: returns original if enhancement fails.
378
+ """
379
+ try:
380
+ return enhance_image_with_codeformer(rgb_img)
381
+ except Exception as e:
382
+ logger.error(f"CodeFormer failed, returning original: {e}")
383
+ return rgb_img
384
 
385
  # --------------------- API Endpoints ---------------------
386
  @fastapi_app.get("/")
 
779
  media_type="image/png",
780
  headers={"Content-Disposition": "inline; filename=result.png"}
781
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
 
783
+ @fastapi_app.post("/multi-face-swap", dependencies=[Depends(verify_token)])
784
+ async def multi_face_swap_api(
785
+ source_image: UploadFile = File(...),
786
+ target_image: UploadFile = File(...)
787
+ ):
788
+ start_time = datetime.utcnow()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
+ try:
791
+ # -----------------------------
792
+ # Read images
793
+ # -----------------------------
794
+ src_bytes = await source_image.read()
795
+ tgt_bytes = await target_image.read()
796
 
797
+ src_bgr = cv2.imdecode(np.frombuffer(src_bytes, np.uint8), cv2.IMREAD_COLOR)
798
+ tgt_bgr = cv2.imdecode(np.frombuffer(tgt_bytes, np.uint8), cv2.IMREAD_COLOR)
 
 
 
 
799
 
800
+ if src_bgr is None or tgt_bgr is None:
801
+ raise HTTPException(400, "Invalid image data")
 
 
802
 
803
+ src_rgb = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB)
804
+ tgt_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
 
805
 
806
+ # -----------------------------
807
+ # Multi-face swap
808
+ # -----------------------------
809
+ swapped_rgb = multi_face_swap(src_rgb, tgt_rgb)
810
 
811
+ # -----------------------------
812
+ # 🔥 MANDATORY ENHANCEMENT
813
+ # -----------------------------
814
+ final_rgb = mandatory_enhancement(swapped_rgb)
 
815
 
816
+ final_bgr = cv2.cvtColor(final_rgb, cv2.COLOR_RGB2BGR)
 
817
 
818
+ # -----------------------------
819
+ # Save temp result
820
+ # -----------------------------
821
+ temp_dir = tempfile.mkdtemp(prefix="multi_faceswap_")
822
+ result_path = os.path.join(temp_dir, "result.png")
823
+ cv2.imwrite(result_path, final_bgr)
824
 
825
+ with open(result_path, "rb") as f:
826
+ result_bytes = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
 
828
+ # -----------------------------
829
+ # Upload
830
+ # -----------------------------
831
+ result_key = f"faceswap/multi/{uuid.uuid4().hex}.png"
832
+ result_url = upload_to_spaces(
833
+ result_bytes,
834
+ result_key,
835
+ content_type="image/png"
836
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
 
838
+ return {
839
+ "result_key": result_key,
840
+ "result_url": result_url
841
+ }
 
 
 
 
 
842
 
843
+ except Exception as e:
844
+ raise HTTPException(status_code=500, detail=str(e))
845
 
846
+ # --------------------- Mount Gradio ---------------------
 
847
 
848
+ multi_faceswap_app = build_multi_faceswap_gradio()
849
+ fastapi_app = mount_gradio_app(
850
+ fastapi_app,
851
+ multi_faceswap_app,
852
+ path="/gradio-multi"
853
+ )
854
 
855
+ if __name__ == "__main__":
856
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)