LogicGoInfotechSpaces commited on
Commit
970adae
·
verified ·
1 Parent(s): 685f07a

Update api/main.py

Browse files
Files changed (1) hide show
  1. api/main.py +947 -2
api/main.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import uuid
4
  import shutil
@@ -123,6 +122,8 @@ def _build_ai_edit_daily_count(
123
  - Case B (today already recorded): return list unchanged
124
  - Case C (gap in days): fill missing days with count=0 and append today with count=1
125
 
 
 
126
  The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
127
  """
128
 
@@ -192,6 +193,10 @@ def _build_ai_edit_daily_count(
192
  }
193
  )
194
 
 
 
 
 
195
  return result
196
 
197
  def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
@@ -935,4 +940,944 @@ def view_result(filename: str):
935
 
936
  @app.get("/logs")
937
  def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
938
- return JSONResponse(content=logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import uuid
3
  import shutil
 
122
  - Case B (today already recorded): return list unchanged
123
  - Case C (gap in days): fill missing days with count=0 and append today with count=1
124
 
125
+ Additionally, the returned list is capped to the most recent 32 entries.
126
+
127
  The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
128
  """
129
 
 
193
  }
194
  )
195
 
196
+ # Enforce 32-entry limit (keep the most recent 32 days)
197
+ if len(result) > 32:
198
+ result = result[-32:]
199
+
200
  return result
201
 
202
  def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
 
940
 
941
  @app.get("/logs")
942
  def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
943
+ return JSONResponse(content=logs)
944
+
945
+
946
+
947
+ # import os
948
+ # import uuid
949
+ # import shutil
950
+ # import re
951
+ # from datetime import datetime, timedelta, date
952
+ # from typing import Dict, List, Optional
953
+
954
+ # import numpy as np
955
+ # from fastapi import (
956
+ # FastAPI,
957
+ # UploadFile,
958
+ # File,
959
+ # HTTPException,
960
+ # Depends,
961
+ # Header,
962
+ # Request,
963
+ # Form,
964
+ # )
965
+ # from fastapi.responses import FileResponse, JSONResponse
966
+ # from pydantic import BaseModel
967
+ # from PIL import Image
968
+ # import cv2
969
+ # import logging
970
+
971
+ # from bson import ObjectId
972
+ # from pymongo import MongoClient
973
+ # import time
974
+
975
+ # logging.basicConfig(level=logging.INFO)
976
+ # log = logging.getLogger("api")
977
+
978
+ # from src.core import process_inpaint
979
+
980
+ # # Directories (use writable space on HF Spaces)
981
+ # BASE_DIR = os.environ.get("DATA_DIR", "/data")
982
+ # if not os.path.isdir(BASE_DIR):
983
+ # # Fallback to /tmp if /data not available
984
+ # BASE_DIR = "/tmp"
985
+
986
+ # UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
987
+ # OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
988
+
989
+ # os.makedirs(UPLOAD_DIR, exist_ok=True)
990
+ # os.makedirs(OUTPUT_DIR, exist_ok=True)
991
+
992
+ # # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
993
+ # ENV_TOKEN = os.environ.get("API_TOKEN")
994
+
995
+ # app = FastAPI(title="Photo Object Removal API", version="1.0.0")
996
+
997
+ # # In-memory stores
998
+ # file_store: Dict[str, Dict[str, str]] = {}
999
+ # logs: List[Dict[str, str]] = []
1000
+
1001
+ # MONGO_URI = "mongodb+srv://harilogicgo_db_user:[email protected]/?appName=KiddoImages"
1002
+ # mongo_client = MongoClient(MONGO_URI)
1003
+ # mongo_db = mongo_client["object_remover"]
1004
+ # mongo_logs = mongo_db["api_logs"]
1005
+
1006
+ # ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
1007
+ # DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
1008
+ # admin_media_clicks = None
1009
+
1010
+
1011
+ # def _init_admin_mongo() -> None:
1012
+ # global admin_media_clicks
1013
+ # if not ADMIN_MONGO_URI:
1014
+ # log.info("Admin Mongo URI not provided; media click logging disabled")
1015
+ # return
1016
+ # try:
1017
+ # admin_client = MongoClient(ADMIN_MONGO_URI)
1018
+ # # get_default_database() extracts database from connection string (e.g., /adminPanel)
1019
+ # admin_db = admin_client.get_default_database()
1020
+ # if admin_db is None:
1021
+ # # Fallback if no database in URI
1022
+ # admin_db = admin_client["admin"]
1023
+ # log.warning("No database in connection string, defaulting to 'admin'")
1024
+
1025
+ # admin_media_clicks = admin_db["media_clicks"]
1026
+ # log.info(
1027
+ # "Admin media click logging initialized: db=%s collection=%s",
1028
+ # admin_db.name,
1029
+ # admin_media_clicks.name,
1030
+ # )
1031
+ # try:
1032
+ # admin_media_clicks.drop_index("user_id_1_header_1_media_id_1")
1033
+ # log.info("Dropped legacy index user_id_1_header_1_media_id_1")
1034
+ # except Exception as idx_err:
1035
+ # # Index drop failure is non-critical (often permission issue)
1036
+ # if "Unauthorized" not in str(idx_err):
1037
+ # log.info("Skipping legacy index drop: %s", idx_err)
1038
+ # except Exception as err:
1039
+ # log.error("Failed to init admin Mongo client: %s", err)
1040
+ # admin_media_clicks = None
1041
+
1042
+
1043
+ # _init_admin_mongo()
1044
+
1045
+
1046
+ # def _admin_logging_status() -> Dict[str, object]:
1047
+ # if admin_media_clicks is None:
1048
+ # return {
1049
+ # "enabled": False,
1050
+ # "db": None,
1051
+ # "collection": None,
1052
+ # }
1053
+ # return {
1054
+ # "enabled": True,
1055
+ # "db": admin_media_clicks.database.name,
1056
+ # "collection": admin_media_clicks.name,
1057
+ # }
1058
+
1059
+
1060
+ # def _build_ai_edit_daily_count(
1061
+ # existing: Optional[List[Dict[str, object]]],
1062
+ # today: date,
1063
+ # ) -> List[Dict[str, object]]:
1064
+ # """
1065
+ # Build / extend the ai_edit_daily_count array with the following rules:
1066
+
1067
+ # - Case A (no existing data): return [{date: today, count: 1}]
1068
+ # - Case B (today already recorded): return list unchanged
1069
+ # - Case C (gap in days): fill missing days with count=0 and append today with count=1
1070
+
1071
+ # The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
1072
+ # """
1073
+
1074
+ # def _to_date_only(value: object) -> date:
1075
+ # if isinstance(value, datetime):
1076
+ # return value.date()
1077
+ # if isinstance(value, date):
1078
+ # return value
1079
+ # # Fallback: try parsing ISO string "YYYY-MM-DD" or full datetime
1080
+ # try:
1081
+ # text = str(value)
1082
+ # if len(text) == 10:
1083
+ # return datetime.strptime(text, "%Y-%m-%d").date()
1084
+ # return datetime.fromisoformat(text).date()
1085
+ # except Exception:
1086
+ # # If parsing fails, just treat as today to avoid crashing
1087
+ # return today
1088
+
1089
+ # # Case A: first ever use (no array yet)
1090
+ # if not existing:
1091
+ # return [
1092
+ # {
1093
+ # "date": datetime(today.year, today.month, today.day),
1094
+ # "count": 1,
1095
+ # }
1096
+ # ]
1097
+
1098
+ # # Work on a shallow copy so we don't mutate original in-place
1099
+ # result: List[Dict[str, object]] = list(existing)
1100
+
1101
+ # last_entry = result[-1] if result else None
1102
+ # if not last_entry or "date" not in last_entry:
1103
+ # # If structure is unexpected, re-initialize safely
1104
+ # return [
1105
+ # {
1106
+ # "date": datetime(today.year, today.month, today.day),
1107
+ # "count": 1,
1108
+ # }
1109
+ # ]
1110
+
1111
+ # last_date = _to_date_only(last_entry["date"])
1112
+
1113
+ # # If somehow the last stored date is in the future, do nothing to avoid corrupting history
1114
+ # if last_date > today:
1115
+ # return result
1116
+
1117
+ # # Case B: today's date already present as the last entry → unchanged
1118
+ # if last_date == today:
1119
+ # return result
1120
+
1121
+ # # Case C: there is a gap, fill missing days with count=0 and append today with count=1
1122
+ # cursor = last_date + timedelta(days=1)
1123
+ # while cursor < today:
1124
+ # result.append(
1125
+ # {
1126
+ # "date": datetime(cursor.year, cursor.month, cursor.day),
1127
+ # "count": 0,
1128
+ # }
1129
+ # )
1130
+ # cursor += timedelta(days=1)
1131
+
1132
+ # # Finally add today's presence indicator
1133
+ # result.append(
1134
+ # {
1135
+ # "date": datetime(today.year, today.month, today.day),
1136
+ # "count": 1,
1137
+ # }
1138
+ # )
1139
+
1140
+ # return result
1141
+
1142
+ # def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
1143
+ # if not ENV_TOKEN:
1144
+ # return
1145
+ # if authorization is None or not authorization.lower().startswith("bearer "):
1146
+ # raise HTTPException(status_code=401, detail="Unauthorized")
1147
+ # token = authorization.split(" ", 1)[1]
1148
+ # if token != ENV_TOKEN:
1149
+ # raise HTTPException(status_code=403, detail="Forbidden")
1150
+
1151
+
1152
+ # class InpaintRequest(BaseModel):
1153
+ # image_id: str
1154
+ # mask_id: str
1155
+ # invert_mask: bool = True # True => selected/painted area is removed
1156
+ # passthrough: bool = False # If True, return the original image unchanged
1157
+ # user_id: Optional[str] = None
1158
+ # category_id: Optional[str] = None
1159
+
1160
+
1161
+ # class SimpleRemoveRequest(BaseModel):
1162
+ # image_id: str # Image with pink/magenta segments to remove
1163
+
1164
+
1165
+ # def _coerce_object_id(value: Optional[str]) -> ObjectId:
1166
+ # if value is None:
1167
+ # return ObjectId()
1168
+ # value_str = str(value).strip()
1169
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", value_str):
1170
+ # return ObjectId(value_str)
1171
+ # if value_str.isdigit():
1172
+ # hex_str = format(int(value_str), "x")
1173
+ # if len(hex_str) > 24:
1174
+ # hex_str = hex_str[-24:]
1175
+ # hex_str = hex_str.rjust(24, "0")
1176
+ # return ObjectId(hex_str)
1177
+ # return ObjectId()
1178
+
1179
+
1180
+ # def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
1181
+ # raw = category_id or DEFAULT_CATEGORY_ID
1182
+ # raw_str = str(raw).strip()
1183
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", raw_str):
1184
+ # return ObjectId(raw_str)
1185
+ # return _coerce_object_id(raw_str)
1186
+
1187
+
1188
+ # def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
1189
+ # """Log to admin media_clicks collection only if user_id is provided."""
1190
+ # if admin_media_clicks is None:
1191
+ # return
1192
+ # # Only log if user_id is provided (not None/empty)
1193
+ # if not user_id or not user_id.strip():
1194
+ # return
1195
+ # try:
1196
+ # user_obj = _coerce_object_id(user_id)
1197
+ # category_obj = _coerce_category_id(category_id)
1198
+ # now = datetime.utcnow()
1199
+ # today = now.date()
1200
+
1201
+ # doc = admin_media_clicks.find_one({"userId": user_obj})
1202
+ # if doc:
1203
+ # existing_daily = doc.get("ai_edit_daily_count")
1204
+ # updated_daily = _build_ai_edit_daily_count(existing_daily, today)
1205
+ # categories = doc.get("categories") or []
1206
+ # if any(cat.get("categoryId") == category_obj for cat in categories):
1207
+ # # Category exists: increment click_count and ai_edit_complete, update dates
1208
+ # admin_media_clicks.update_one(
1209
+ # {"_id": doc["_id"], "categories.categoryId": category_obj},
1210
+ # {
1211
+ # "$inc": {
1212
+ # "categories.$.click_count": 1,
1213
+ # "ai_edit_complete": 1, # $inc handles missing fields (backward compatible)
1214
+ # },
1215
+ # "$set": {
1216
+ # "categories.$.lastClickedAt": now,
1217
+ # "updatedAt": now,
1218
+ # "ai_edit_last_date": now,
1219
+ # "ai_edit_daily_count": updated_daily,
1220
+ # },
1221
+ # },
1222
+ # )
1223
+ # else:
1224
+ # # New category to existing document: push category, increment ai_edit_complete
1225
+ # admin_media_clicks.update_one(
1226
+ # {"_id": doc["_id"]},
1227
+ # {
1228
+ # "$push": {
1229
+ # "categories": {
1230
+ # "categoryId": category_obj,
1231
+ # "click_count": 1,
1232
+ # "lastClickedAt": now,
1233
+ # }
1234
+ # },
1235
+ # "$inc": {"ai_edit_complete": 1}, # $inc handles missing fields
1236
+ # "$set": {
1237
+ # "updatedAt": now,
1238
+ # "ai_edit_last_date": now,
1239
+ # "ai_edit_daily_count": updated_daily,
1240
+ # },
1241
+ # },
1242
+ # )
1243
+ # else:
1244
+ # # New user: create document with default ai_edit_complete=0, then increment to 1
1245
+ # daily_for_new = _build_ai_edit_daily_count(None, today)
1246
+ # admin_media_clicks.update_one(
1247
+ # {"userId": user_obj},
1248
+ # {
1249
+ # "$setOnInsert": {
1250
+ # "userId": user_obj,
1251
+ # "categories": [
1252
+ # {
1253
+ # "categoryId": category_obj,
1254
+ # "click_count": 1,
1255
+ # "lastClickedAt": now,
1256
+ # }
1257
+ # ],
1258
+ # "createdAt": now,
1259
+ # "updatedAt": now,
1260
+ # "ai_edit_complete": 0, # Default for new users
1261
+ # "ai_edit_daily_count": daily_for_new,
1262
+ # },
1263
+ # "$inc": {"ai_edit_complete": 1}, # Increment to 1 on first use
1264
+ # "$set": {
1265
+ # "updatedAt": now,
1266
+ # "ai_edit_last_date": now,
1267
+ # },
1268
+ # },
1269
+ # upsert=True,
1270
+ # )
1271
+ # except Exception as err:
1272
+ # err_str = str(err)
1273
+ # if "Unauthorized" in err_str or "not authorized" in err_str.lower():
1274
+ # log.warning(
1275
+ # "Admin media click logging failed (permissions): user lacks read/write on db=%s collection=%s. "
1276
+ # "Check MongoDB user permissions.",
1277
+ # admin_media_clicks.database.name,
1278
+ # admin_media_clicks.name,
1279
+ # )
1280
+ # else:
1281
+ # log.warning("Admin media click logging failed: %s", err)
1282
+
1283
+
1284
+ # @app.get("/")
1285
+ # def root() -> Dict[str, object]:
1286
+ # return {
1287
+ # "name": "Photo Object Removal API",
1288
+ # "status": "ok",
1289
+ # "endpoints": {
1290
+ # "GET /health": "health check",
1291
+ # "POST /upload-image": "form-data: image=file",
1292
+ # "POST /upload-mask": "form-data: mask=file",
1293
+ # "POST /inpaint": "JSON: {image_id, mask_id}",
1294
+ # "POST /inpaint-multipart": "form-data: image=file, mask=file",
1295
+ # "POST /remove-pink": "form-data: image=file (auto-detects pink segments and removes them)",
1296
+ # "GET /download/{filename}": "download result image",
1297
+ # "GET /result/{filename}": "view result image in browser",
1298
+ # "GET /logs": "recent uploads/results",
1299
+ # },
1300
+ # "auth": "set API_TOKEN env var to require Authorization: Bearer <token> (except /health)",
1301
+ # }
1302
+
1303
+
1304
+ # @app.get("/health")
1305
+ # def health() -> Dict[str, str]:
1306
+ # return {"status": "healthy"}
1307
+
1308
+
1309
+ # @app.get("/logging-status")
1310
+ # def logging_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
1311
+ # """Helper endpoint to verify admin media logging wiring (no secrets exposed)."""
1312
+ # return _admin_logging_status()
1313
+
1314
+
1315
+ # @app.post("/upload-image")
1316
+ # def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
1317
+ # ext = os.path.splitext(image.filename)[1] or ".png"
1318
+ # file_id = str(uuid.uuid4())
1319
+ # stored_name = f"{file_id}{ext}"
1320
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
1321
+ # with open(stored_path, "wb") as f:
1322
+ # shutil.copyfileobj(image.file, f)
1323
+ # file_store[file_id] = {
1324
+ # "type": "image",
1325
+ # "filename": image.filename,
1326
+ # "stored_name": stored_name,
1327
+ # "path": stored_path,
1328
+ # "timestamp": datetime.utcnow().isoformat(),
1329
+ # }
1330
+ # logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
1331
+ # return {"id": file_id, "filename": image.filename}
1332
+
1333
+
1334
+ # @app.post("/upload-mask")
1335
+ # def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
1336
+ # ext = os.path.splitext(mask.filename)[1] or ".png"
1337
+ # file_id = str(uuid.uuid4())
1338
+ # stored_name = f"{file_id}{ext}"
1339
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
1340
+ # with open(stored_path, "wb") as f:
1341
+ # shutil.copyfileobj(mask.file, f)
1342
+ # file_store[file_id] = {
1343
+ # "type": "mask",
1344
+ # "filename": mask.filename,
1345
+ # "stored_name": stored_name,
1346
+ # "path": stored_path,
1347
+ # "timestamp": datetime.utcnow().isoformat(),
1348
+ # }
1349
+ # logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
1350
+ # return {"id": file_id, "filename": mask.filename}
1351
+
1352
+
1353
+ # def _load_rgba_image(path: str) -> Image.Image:
1354
+ # img = Image.open(path)
1355
+ # return img.convert("RGBA")
1356
+
1357
+
1358
+ # def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
1359
+ # """
1360
+ # Convert mask image to RGBA format (black/white mask).
1361
+ # Standard convention: white (255) = area to remove, black (0) = area to keep
1362
+ # Returns RGBA with white in RGB channels where removal is needed, alpha=255
1363
+ # """
1364
+ # if img.mode != "RGBA":
1365
+ # # For RGB/Grayscale masks: white (value>128) = remove, black (value<=128) = keep
1366
+ # gray = img.convert("L")
1367
+ # arr = np.array(gray)
1368
+ # # Create proper black/white mask: white pixels (>128) = remove, black (<=128) = keep
1369
+ # mask_bw = np.where(arr > 128, 255, 0).astype(np.uint8)
1370
+
1371
+ # rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
1372
+ # rgba[:, :, 0] = mask_bw # R
1373
+ # rgba[:, :, 1] = mask_bw # G
1374
+ # rgba[:, :, 2] = mask_bw # B
1375
+ # rgba[:, :, 3] = 255 # Fully opaque
1376
+ # log.info(f"Loaded {img.mode} mask: {int((mask_bw > 0).sum())} white pixels (to remove)")
1377
+ # return rgba
1378
+
1379
+ # # For RGBA: check if alpha channel is meaningful
1380
+ # arr = np.array(img)
1381
+ # alpha = arr[:, :, 3]
1382
+ # rgb = arr[:, :, :3]
1383
+
1384
+ # # If alpha is mostly opaque everywhere (mean > 200), treat RGB channels as mask values
1385
+ # if alpha.mean() > 200:
1386
+ # # Use RGB to determine mask: white/bright in RGB = remove
1387
+ # gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
1388
+ # # Also detect magenta specifically
1389
+ # magenta = np.all(rgb == [255, 0, 255], axis=2).astype(np.uint8) * 255
1390
+ # mask_bw = np.maximum(np.where(gray > 128, 255, 0).astype(np.uint8), magenta)
1391
+
1392
+ # rgba = arr.copy()
1393
+ # rgba[:, :, 0] = mask_bw # R
1394
+ # rgba[:, :, 1] = mask_bw # G
1395
+ # rgba[:, :, 2] = mask_bw # B
1396
+ # rgba[:, :, 3] = 255 # Fully opaque
1397
+ # log.info(f"Loaded RGBA mask (RGB-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
1398
+ # return rgba
1399
+
1400
+ # # Alpha channel encodes the mask - convert to RGB-based
1401
+ # # Transparent areas (alpha < 128) = remove, Opaque areas = keep
1402
+ # mask_bw = np.where(alpha < 128, 255, 0).astype(np.uint8)
1403
+ # rgba = arr.copy()
1404
+ # rgba[:, :, 0] = mask_bw
1405
+ # rgba[:, :, 1] = mask_bw
1406
+ # rgba[:, :, 2] = mask_bw
1407
+ # rgba[:, :, 3] = 255
1408
+ # log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
1409
+ # return rgba
1410
+
1411
+ # @app.post("/inpaint")
1412
+ # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
1413
+ # start_time = time.time()
1414
+ # status = "success"
1415
+ # error_msg = None
1416
+ # output_name = None
1417
+
1418
+ # try:
1419
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
1420
+ # raise HTTPException(status_code=404, detail="image_id not found")
1421
+
1422
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
1423
+ # raise HTTPException(status_code=404, detail="mask_id not found")
1424
+
1425
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
1426
+ # mask_img = Image.open(file_store[req.mask_id]["path"])
1427
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
1428
+
1429
+ # if req.passthrough:
1430
+ # result = np.array(img_rgba.convert("RGB"))
1431
+ # else:
1432
+ # result = process_inpaint(
1433
+ # np.array(img_rgba),
1434
+ # mask_rgba,
1435
+ # invert_mask=req.invert_mask
1436
+ # )
1437
+
1438
+ # output_name = f"output_{uuid.uuid4().hex}.png"
1439
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
1440
+
1441
+ # Image.fromarray(result).save(
1442
+ # output_path, "PNG", optimize=False, compress_level=1
1443
+ # )
1444
+
1445
+ # log_media_click(req.user_id, req.category_id)
1446
+ # return {"result": output_name}
1447
+
1448
+ # except Exception as e:
1449
+ # status = "fail"
1450
+ # error_msg = str(e)
1451
+ # raise
1452
+
1453
+ # finally:
1454
+ # end_time = time.time()
1455
+ # response_time_ms = (end_time - start_time) * 1000
1456
+
1457
+ # log_doc = {
1458
+ # "input_image_id": req.image_id,
1459
+ # "input_mask_id": req.mask_id,
1460
+ # "output_id": output_name,
1461
+ # "status": status,
1462
+ # "timestamp": datetime.utcnow(),
1463
+ # "ts": int(time.time()),
1464
+ # "response_time_ms": response_time_ms
1465
+ # }
1466
+
1467
+ # if error_msg:
1468
+ # log_doc["error"] = error_msg
1469
+
1470
+ # try:
1471
+ # mongo_logs.insert_one(log_doc)
1472
+ # except Exception as mongo_err:
1473
+ # log.error(f"Mongo log insert failed: {mongo_err}")
1474
+
1475
+ # # @app.post("/inpaint")
1476
+ # # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
1477
+ # # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
1478
+ # # raise HTTPException(status_code=404, detail="image_id not found")
1479
+ # # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
1480
+ # # raise HTTPException(status_code=404, detail="mask_id not found")
1481
+
1482
+ # # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
1483
+ # # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
1484
+ # # mask_rgba = _load_rgba_mask_from_image(mask_img)
1485
+
1486
+ # # # Debug: check mask before processing
1487
+ # # white_pixels = int((mask_rgba[:,:,0] > 128).sum())
1488
+ # # log.info(f"Inpaint request: mask has {white_pixels} white pixels, invert_mask={req.invert_mask}")
1489
+
1490
+ # # if req.passthrough:
1491
+ # # result = np.array(img_rgba.convert("RGB"))
1492
+ # # else:
1493
+ # # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
1494
+ # # result_name = f"output_{uuid.uuid4().hex}.png"
1495
+ # # result_path = os.path.join(OUTPUT_DIR, result_name)
1496
+ # # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1497
+
1498
+ # # logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
1499
+ # # return {"result": result_name}
1500
+
1501
+
1502
+ # @app.post("/inpaint-url")
1503
+ # def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
1504
+ # """Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
1505
+ # start_time = time.time()
1506
+ # status = "success"
1507
+ # error_msg = None
1508
+ # result_name = None
1509
+
1510
+ # try:
1511
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
1512
+ # raise HTTPException(status_code=404, detail="image_id not found")
1513
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
1514
+ # raise HTTPException(status_code=404, detail="mask_id not found")
1515
+
1516
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
1517
+ # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
1518
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
1519
+
1520
+ # if req.passthrough:
1521
+ # result = np.array(img_rgba.convert("RGB"))
1522
+ # else:
1523
+ # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
1524
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1525
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1526
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1527
+
1528
+ # url = str(request.url_for("download_file", filename=result_name))
1529
+ # logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
1530
+ # log_media_click(req.user_id, req.category_id)
1531
+ # return {"result": result_name, "url": url}
1532
+ # except Exception as e:
1533
+ # status = "fail"
1534
+ # error_msg = str(e)
1535
+ # raise
1536
+ # finally:
1537
+ # # Always log to regular MongoDB (mandatory)
1538
+ # end_time = time.time()
1539
+ # response_time_ms = (end_time - start_time) * 1000
1540
+ # log_doc = {
1541
+ # "input_image_id": req.image_id,
1542
+ # "input_mask_id": req.mask_id,
1543
+ # "output_id": result_name,
1544
+ # "status": status,
1545
+ # "timestamp": datetime.utcnow(),
1546
+ # "ts": int(time.time()),
1547
+ # "response_time_ms": response_time_ms,
1548
+ # }
1549
+ # if error_msg:
1550
+ # log_doc["error"] = error_msg
1551
+ # try:
1552
+ # mongo_logs.insert_one(log_doc)
1553
+ # except Exception as mongo_err:
1554
+ # log.error("Mongo log insert failed: %s", mongo_err)
1555
+
1556
+
1557
+ # @app.post("/inpaint-multipart")
1558
+ # def inpaint_multipart(
1559
+ # image: UploadFile = File(...),
1560
+ # mask: UploadFile = File(...),
1561
+ # request: Request = None,
1562
+ # invert_mask: bool = True,
1563
+ # mask_is_painted: bool = False, # if True, mask file is the painted-on image (e.g., black strokes on original)
1564
+ # passthrough: bool = False,
1565
+ # user_id: Optional[str] = Form(None),
1566
+ # category_id: Optional[str] = Form(None),
1567
+ # _: None = Depends(bearer_auth),
1568
+ # ) -> Dict[str, str]:
1569
+ # start_time = time.time()
1570
+ # status = "success"
1571
+ # error_msg = None
1572
+ # result_name = None
1573
+
1574
+ # try:
1575
+ # # Load in-memory
1576
+ # img = Image.open(image.file).convert("RGBA")
1577
+ # m = Image.open(mask.file).convert("RGBA")
1578
+
1579
+ # if passthrough:
1580
+ # # Just echo the input image, ignore mask
1581
+ # result = np.array(img.convert("RGB"))
1582
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1583
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1584
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1585
+
1586
+ # url: Optional[str] = None
1587
+ # try:
1588
+ # if request is not None:
1589
+ # url = str(request.url_for("download_file", filename=result_name))
1590
+ # except Exception:
1591
+ # url = None
1592
+
1593
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
1594
+ # if url:
1595
+ # entry["url"] = url
1596
+ # logs.append(entry)
1597
+ # resp: Dict[str, str] = {"result": result_name}
1598
+ # if url:
1599
+ # resp["url"] = url
1600
+ # log_media_click(user_id, category_id)
1601
+ # return resp
1602
+
1603
+ # if mask_is_painted:
1604
+ # # Auto-detect pink/magenta paint and convert to black/white mask
1605
+ # # White pixels = areas to remove, Black pixels = areas to keep
1606
+ # log.info("Auto-detecting pink/magenta paint from uploaded image...")
1607
+
1608
+ # m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
1609
+
1610
+ # # Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
1611
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
1612
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
1613
+ # magenta_detected = (
1614
+ # (m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
1615
+ # (m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
1616
+ # (m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
1617
+ # ).astype(np.uint8) * 255
1618
+
1619
+ # # Method 2: Also check if original image was provided to find differences
1620
+ # if img is not None:
1621
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
1622
+ # if img_rgb.shape == m_rgb.shape:
1623
+ # diff = cv2.absdiff(img_rgb, m_rgb)
1624
+ # gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
1625
+ # # Any significant difference (>50) could be paint
1626
+ # diff_mask = (gray_diff > 50).astype(np.uint8) * 255
1627
+ # # Combine with magenta detection
1628
+ # binmask = cv2.bitwise_or(magenta_detected, diff_mask)
1629
+ # else:
1630
+ # binmask = magenta_detected
1631
+ # else:
1632
+ # # No original image provided, use magenta detection only
1633
+ # binmask = magenta_detected
1634
+
1635
+ # # Clean up the mask: remove noise and fill small holes
1636
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
1637
+ # # Close small gaps in the mask
1638
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
1639
+ # # Remove small noise
1640
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
1641
+
1642
+ # nonzero = int((binmask > 0).sum())
1643
+ # log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
1644
+
1645
+ # # If very few pixels detected, assume the user may already be providing a BW mask
1646
+ # # and proceed without forcing strict detection
1647
+
1648
+ # if nonzero < 50:
1649
+ # log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
1650
+ # result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
1651
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1652
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1653
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1654
+ # return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
1655
+
1656
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1657
+ # # Encode in RGBA format for process_inpaint
1658
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
1659
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
1660
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
1661
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
1662
+ # mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
1663
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
1664
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
1665
+ # # Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
1666
+ # mask_rgba[:, :, 3] = 255 - binmask
1667
+
1668
+ # log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
1669
+ # nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
1670
+ # else:
1671
+ # mask_rgba = _load_rgba_mask_from_image(m)
1672
+
1673
+ # # When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
1674
+ # actual_invert = invert_mask # Use default True for painted masks
1675
+ # log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
1676
+
1677
+ # result = process_inpaint(np.array(img), mask_rgba, invert_mask=actual_invert)
1678
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1679
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1680
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1681
+
1682
+ # url: Optional[str] = None
1683
+ # try:
1684
+ # if request is not None:
1685
+ # url = str(request.url_for("download_file", filename=result_name))
1686
+ # except Exception:
1687
+ # url = None
1688
+
1689
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
1690
+ # if url:
1691
+ # entry["url"] = url
1692
+ # logs.append(entry)
1693
+ # resp: Dict[str, str] = {"result": result_name}
1694
+ # if url:
1695
+ # resp["url"] = url
1696
+ # log_media_click(user_id, category_id)
1697
+ # return resp
1698
+ # except Exception as e:
1699
+ # status = "fail"
1700
+ # error_msg = str(e)
1701
+ # raise
1702
+ # finally:
1703
+ # # Always log to regular MongoDB (mandatory)
1704
+ # end_time = time.time()
1705
+ # response_time_ms = (end_time - start_time) * 1000
1706
+ # log_doc = {
1707
+ # "endpoint": "inpaint-multipart",
1708
+ # "output_id": result_name,
1709
+ # "status": status,
1710
+ # "timestamp": datetime.utcnow(),
1711
+ # "ts": int(time.time()),
1712
+ # "response_time_ms": response_time_ms,
1713
+ # }
1714
+ # if error_msg:
1715
+ # log_doc["error"] = error_msg
1716
+ # try:
1717
+ # mongo_logs.insert_one(log_doc)
1718
+ # except Exception as mongo_err:
1719
+ # log.error("Mongo log insert failed: %s", mongo_err)
1720
+
1721
+
1722
+ # @app.post("/remove-pink")
1723
+ # def remove_pink_segments(
1724
+ # image: UploadFile = File(...),
1725
+ # request: Request = None,
1726
+ # user_id: Optional[str] = Form(None),
1727
+ # category_id: Optional[str] = Form(None),
1728
+ # _: None = Depends(bearer_auth),
1729
+ # ) -> Dict[str, str]:
1730
+ # """
1731
+ # Simple endpoint: upload an image with pink/magenta segments to remove.
1732
+ # - Pink/Magenta segments → automatically removed (white in mask)
1733
+ # - Everything else → automatically kept (black in mask)
1734
+ # Just paint pink/magenta on areas you want to remove, upload the image, and it works!
1735
+ # """
1736
+ # start_time = time.time()
1737
+ # status = "success"
1738
+ # error_msg = None
1739
+ # result_name = None
1740
+
1741
+ # try:
1742
+ # log.info(f"Simple remove-pink: processing image {image.filename}")
1743
+
1744
+ # # Load the image (with pink paint on it)
1745
+ # img = Image.open(image.file).convert("RGBA")
1746
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
1747
+
1748
+ # # Auto-detect pink/magenta segments to remove
1749
+ # # Pink/Magenta → white in mask (remove)
1750
+ # # Everything else (natural image colors, including dark areas) → black in mask (keep)
1751
+
1752
+ # # Detect pink/magenta using fixed RGB bounds per requested logic
1753
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
1754
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
1755
+ # binmask = (
1756
+ # (img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
1757
+ # (img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
1758
+ # (img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
1759
+ # ).astype(np.uint8) * 255
1760
+
1761
+ # # Clean up the pink mask
1762
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
1763
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
1764
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
1765
+
1766
+ # nonzero = int((binmask > 0).sum())
1767
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
1768
+ # log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
1769
+
1770
+ # # Debug: log bounds used
1771
+ # log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
1772
+
1773
+ # if nonzero < 50:
1774
+ # log.error("No pink segments detected! Returning original image.")
1775
+ # result = np.array(img.convert("RGB"))
1776
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1777
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1778
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1779
+ # return {
1780
+ # "result": result_name,
1781
+ # "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
1782
+ # }
1783
+
1784
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1785
+ # # Encode in RGBA format that process_inpaint expects
1786
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
1787
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
1788
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
1789
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
1790
+ # # RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
1791
+ # mask_rgba[:, :, 0] = binmask # R: white where pink
1792
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
1793
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
1794
+ # # Alpha: 0 (transparent) where pink → will become white after 255-alpha
1795
+ # # 255 (opaque) everywhere else → will become black after 255-alpha
1796
+ # mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
1797
+
1798
+ # # Verify mask encoding
1799
+ # alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
1800
+ # alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
1801
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
1802
+ # log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
1803
+ # log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
1804
+
1805
+ # # IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
1806
+ # # Remove pink from the original image before processing
1807
+ # # Create a clean version: where pink was detected, keep original image colors
1808
+ # img_clean = np.array(img.convert("RGBA"))
1809
+ # # Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
1810
+ # # Actually, the model will inpaint over those areas, so we can pass the original
1811
+ # # But for better results, we might want to remove the pink overlay first
1812
+
1813
+ # # Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
1814
+ # log.info(f"Starting inpainting process...")
1815
+ # result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
1816
+ # log.info(f"Inpainting complete, result shape: {result.shape}")
1817
+ # result_name = f"output_{uuid.uuid4().hex}.png"
1818
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
1819
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1820
+
1821
+ # url: Optional[str] = None
1822
+ # try:
1823
+ # if request is not None:
1824
+ # url = str(request.url_for("download_file", filename=result_name))
1825
+ # except Exception:
1826
+ # url = None
1827
+
1828
+ # logs.append({
1829
+ # "result": result_name,
1830
+ # "filename": image.filename,
1831
+ # "pink_pixels": nonzero,
1832
+ # "timestamp": datetime.utcnow().isoformat()
1833
+ # })
1834
+
1835
+ # resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
1836
+ # if url:
1837
+ # resp["url"] = url
1838
+ # log_media_click(user_id, category_id)
1839
+ # return resp
1840
+ # except Exception as e:
1841
+ # status = "fail"
1842
+ # error_msg = str(e)
1843
+ # raise
1844
+ # finally:
1845
+ # # Always log to regular MongoDB (mandatory)
1846
+ # end_time = time.time()
1847
+ # response_time_ms = (end_time - start_time) * 1000
1848
+ # log_doc = {
1849
+ # "endpoint": "remove-pink",
1850
+ # "output_id": result_name,
1851
+ # "status": status,
1852
+ # "timestamp": datetime.utcnow(),
1853
+ # "ts": int(time.time()),
1854
+ # "response_time_ms": response_time_ms,
1855
+ # }
1856
+ # if error_msg:
1857
+ # log_doc["error"] = error_msg
1858
+ # try:
1859
+ # mongo_logs.insert_one(log_doc)
1860
+ # except Exception as mongo_err:
1861
+ # log.error("Mongo log insert failed: %s", mongo_err)
1862
+
1863
+
1864
+ # @app.get("/download/{filename}")
1865
+ # def download_file(filename: str):
1866
+ # path = os.path.join(OUTPUT_DIR, filename)
1867
+ # if not os.path.isfile(path):
1868
+ # raise HTTPException(status_code=404, detail="file not found")
1869
+ # return FileResponse(path)
1870
+
1871
+
1872
+ # @app.get("/result/{filename}")
1873
+ # def view_result(filename: str):
1874
+ # """View result image directly in browser (same as download but with proper content-type for viewing)"""
1875
+ # path = os.path.join(OUTPUT_DIR, filename)
1876
+ # if not os.path.isfile(path):
1877
+ # raise HTTPException(status_code=404, detail="file not found")
1878
+ # return FileResponse(path, media_type="image/png")
1879
+
1880
+
1881
+ # @app.get("/logs")
1882
+ # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
1883
+ # return JSONResponse(content=logs)