LogicGoInfotechSpaces commited on
Commit
14f50a3
·
verified ·
1 Parent(s): af5b2b0

Update api/main.py

Browse files
Files changed (1) hide show
  1. api/main.py +1661 -12
api/main.py CHANGED
@@ -2,6 +2,7 @@ import os
2
  import uuid
3
  import shutil
4
  import re
 
5
  from datetime import datetime, timedelta, date
6
  from io import BytesIO
7
  from typing import Dict, List, Optional,Any
@@ -21,6 +22,7 @@ from pydantic import BaseModel
21
  from PIL import Image, UnidentifiedImageError
22
  import cv2
23
  import logging
 
24
  from gridfs import GridFS
25
  from gridfs.errors import NoFile
26
 
@@ -53,6 +55,18 @@ OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
53
  os.makedirs(UPLOAD_DIR, exist_ok=True)
54
  os.makedirs(OUTPUT_DIR, exist_ok=True)
55
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
57
  ENV_TOKEN = os.environ.get("API_TOKEN")
58
 
@@ -112,6 +126,10 @@ API_LOGS_MONGO_URI = os.environ.get("API_LOGS_MONGODB_URL")
112
  api_logs_client = None
113
  api_logs_db = None
114
  api_logs_collection = None
 
 
 
 
115
 
116
  if API_LOGS_MONGO_URI:
117
  try:
@@ -124,6 +142,29 @@ if API_LOGS_MONGO_URI:
124
  api_logs_collection = None
125
  else:
126
  log.warning("API_LOGS_MONGODB_URL not set. API logging disabled.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
129
  DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
@@ -386,6 +427,110 @@ def _load_rgba_image_from_gridfs(file_id: str, expected_type: str) -> Image.Imag
386
  return img.convert("RGBA")
387
 
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  def _build_ai_edit_daily_count(
390
  existing: Optional[List[Dict[str, object]]],
391
  today: date,
@@ -820,6 +965,7 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
820
  error_msg = None
821
  output_name = None
822
  compressed_url = None
 
823
 
824
  try:
825
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
@@ -846,9 +992,12 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
846
  output_name = f"output_{uuid.uuid4().hex}.png"
847
  output_path = os.path.join(OUTPUT_DIR, output_name)
848
 
849
- Image.fromarray(result).save(
850
- output_path, "PNG", optimize=False, compress_level=1
851
- )
 
 
 
852
 
853
  # Create compressed version
854
  compressed_name = f"compressed_{output_name.replace('.png', '.jpg')}"
@@ -869,6 +1018,9 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
869
  response = {"result": output_name}
870
  if compressed_url:
871
  response["Compressed_Image_URL"] = compressed_url
 
 
 
872
  return response
873
 
874
  except Exception as e:
@@ -904,6 +1056,9 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
904
 
905
  if error_msg:
906
  log_doc["error"] = error_msg
 
 
 
907
 
908
  # if mongo_logs is not None:
909
  # try:
@@ -967,6 +1122,7 @@ def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_
967
  status = "success"
968
  error_msg = None
969
  result_name = None
 
970
 
971
  try:
972
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
@@ -991,12 +1147,21 @@ def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_
991
  )
992
  result_name = f"output_{uuid.uuid4().hex}.png"
993
  result_path = os.path.join(OUTPUT_DIR, result_name)
994
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
 
 
 
 
 
995
 
996
  url = str(request.url_for("download_file", filename=result_name))
997
  logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
998
  log_media_click(req.user_id, category_id, req.appname)
999
- return {"result": result_name, "url": url}
 
 
 
 
1000
  except Exception as e:
1001
  status = "fail"
1002
  error_msg = str(e)
@@ -1019,6 +1184,9 @@ def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_
1019
  log_doc["appname"] = req.appname
1020
  if error_msg:
1021
  log_doc["error"] = error_msg
 
 
 
1022
  if mongo_logs is not None:
1023
  try:
1024
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
@@ -1059,6 +1227,7 @@ def inpaint_multipart(
1059
  status = "success"
1060
  error_msg = None
1061
  result_name = None
 
1062
 
1063
  try:
1064
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
@@ -1077,7 +1246,12 @@ def inpaint_multipart(
1077
  result = np.array(img.convert("RGB"))
1078
  result_name = f"output_{uuid.uuid4().hex}.png"
1079
  result_path = os.path.join(OUTPUT_DIR, result_name)
1080
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
 
 
 
 
 
1081
 
1082
  url: Optional[str] = None
1083
  try:
@@ -1093,6 +1267,9 @@ def inpaint_multipart(
1093
  resp: Dict[str, str] = {"result": result_name}
1094
  if url:
1095
  resp["url"] = url
 
 
 
1096
  log_media_click(user_id, final_category_id, appname)
1097
  return resp
1098
 
@@ -1146,8 +1323,17 @@ def inpaint_multipart(
1146
  result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
1147
  result_name = f"output_{uuid.uuid4().hex}.png"
1148
  result_path = os.path.join(OUTPUT_DIR, result_name)
1149
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1150
- return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
 
 
 
 
 
 
 
 
 
1151
 
1152
  # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1153
  # Encode in RGBA format for process_inpaint
@@ -1178,7 +1364,12 @@ def inpaint_multipart(
1178
  )
1179
  result_name = f"output_{uuid.uuid4().hex}.png"
1180
  result_path = os.path.join(OUTPUT_DIR, result_name)
1181
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
 
 
 
 
 
1182
 
1183
  url: Optional[str] = None
1184
  try:
@@ -1194,6 +1385,9 @@ def inpaint_multipart(
1194
  resp: Dict[str, str] = {"result": result_name}
1195
  if url:
1196
  resp["url"] = url
 
 
 
1197
  log_media_click(user_id, final_category_id, appname)
1198
  return resp
1199
  except Exception as e:
@@ -1217,6 +1411,9 @@ def inpaint_multipart(
1217
  log_doc["appname"] = appname
1218
  if error_msg:
1219
  log_doc["error"] = error_msg
 
 
 
1220
  if mongo_logs is not None:
1221
  try:
1222
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
@@ -1258,6 +1455,7 @@ def remove_pink_segments(
1258
  status = "success"
1259
  error_msg = None
1260
  result_name = None
 
1261
 
1262
  try:
1263
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
@@ -1303,11 +1501,20 @@ def remove_pink_segments(
1303
  result = np.array(img.convert("RGB"))
1304
  result_name = f"output_{uuid.uuid4().hex}.png"
1305
  result_path = os.path.join(OUTPUT_DIR, result_name)
1306
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
1307
- return {
 
 
 
 
 
1308
  "result": result_name,
1309
  "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
1310
  }
 
 
 
 
1311
 
1312
  # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1313
  # Encode in RGBA format that process_inpaint expects
@@ -1344,7 +1551,12 @@ def remove_pink_segments(
1344
  log.info(f"Inpainting complete, result shape: {result.shape}")
1345
  result_name = f"output_{uuid.uuid4().hex}.png"
1346
  result_path = os.path.join(OUTPUT_DIR, result_name)
1347
- Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
 
 
 
 
 
1348
 
1349
  url: Optional[str] = None
1350
  try:
@@ -1363,6 +1575,9 @@ def remove_pink_segments(
1363
  resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
1364
  if url:
1365
  resp["url"] = url
 
 
 
1366
  log_media_click(user_id, final_category_id, appname)
1367
  return resp
1368
  except Exception as e:
@@ -1386,6 +1601,9 @@ def remove_pink_segments(
1386
  log_doc["appname"] = appname
1387
  if error_msg:
1388
  log_doc["error"] = error_msg
 
 
 
1389
  if mongo_logs is not None:
1390
  try:
1391
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
@@ -1428,3 +1646,1434 @@ def view_result(filename: str):
1428
  @app.get("/logs")
1429
  def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
1430
  return JSONResponse(content=logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import uuid
3
  import shutil
4
  import re
5
+ import threading
6
  from datetime import datetime, timedelta, date
7
  from io import BytesIO
8
  from typing import Dict, List, Optional,Any
 
22
  from PIL import Image, UnidentifiedImageError
23
  import cv2
24
  import logging
25
+ import boto3
26
  from gridfs import GridFS
27
  from gridfs.errors import NoFile
28
 
 
55
  os.makedirs(UPLOAD_DIR, exist_ok=True)
56
  os.makedirs(OUTPUT_DIR, exist_ok=True)
57
 
58
+ SPACES_BUCKET = os.environ.get("DO_SPACES_BUCKET", "milestone")
59
+ SPACES_REGION = os.environ.get("DO_SPACES_REGION", "blr1")
60
+ SPACES_ENDPOINT = os.environ.get(
61
+ "DO_SPACES_ENDPOINT",
62
+ f"https://{SPACES_REGION}.digitaloceanspaces.com",
63
+ )
64
+ SPACES_KEY = os.environ.get("DO_SPACES_KEY")
65
+ SPACES_SECRET = os.environ.get("DO_SPACES_SECRET")
66
+ SPACES_ROOT_PREFIX = os.environ.get("DO_SPACES_ROOT_PREFIX", "valentine/objectremover").strip("/")
67
+ SPACES_SOURCE_PREFIX = f"{SPACES_ROOT_PREFIX}/source"
68
+ SPACES_RESULTS_PREFIX = f"{SPACES_ROOT_PREFIX}/results"
69
+
70
  # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
71
  ENV_TOKEN = os.environ.get("API_TOKEN")
72
 
 
126
  api_logs_client = None
127
  api_logs_db = None
128
  api_logs_collection = None
129
+ spaces_client = None
130
+ spaces_enabled = False
131
+ spaces_counter_lock = threading.Lock()
132
+ spaces_pair_counter: Optional[int] = None
133
 
134
  if API_LOGS_MONGO_URI:
135
  try:
 
142
  api_logs_collection = None
143
  else:
144
  log.warning("API_LOGS_MONGODB_URL not set. API logging disabled.")
145
+
146
+ if SPACES_KEY and SPACES_SECRET:
147
+ try:
148
+ spaces_client = boto3.client(
149
+ "s3",
150
+ region_name=SPACES_REGION,
151
+ endpoint_url=SPACES_ENDPOINT,
152
+ aws_access_key_id=SPACES_KEY,
153
+ aws_secret_access_key=SPACES_SECRET,
154
+ )
155
+ spaces_enabled = True
156
+ log.info(
157
+ "DigitalOcean Spaces initialized: bucket=%s source_prefix=%s results_prefix=%s",
158
+ SPACES_BUCKET,
159
+ SPACES_SOURCE_PREFIX,
160
+ SPACES_RESULTS_PREFIX,
161
+ )
162
+ except Exception as err:
163
+ log.error("Failed to initialize DigitalOcean Spaces client: %s", err, exc_info=True)
164
+ spaces_client = None
165
+ spaces_enabled = False
166
+ else:
167
+ log.info("DigitalOcean Spaces not configured; source/result mirroring disabled")
168
 
169
  ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
170
  DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
 
427
  return img.convert("RGBA")
428
 
429
 
430
+ def _pil_to_jpeg_bytes(img: Image.Image, quality: int = 95) -> bytes:
431
+ buf = BytesIO()
432
+ rgb_img = img.convert("RGB")
433
+ rgb_img.save(buf, format="JPEG", quality=quality)
434
+ return buf.getvalue()
435
+
436
+
437
+ def _pil_to_png_bytes(img: Image.Image) -> bytes:
438
+ buf = BytesIO()
439
+ img.save(buf, format="PNG", optimize=False, compress_level=1)
440
+ return buf.getvalue()
441
+
442
+
443
+ def _extract_pair_number_from_key(key: str, prefix: str, file_label: str, ext: str) -> Optional[int]:
444
+ normalized_prefix = prefix.strip("/")
445
+ pattern = rf"^{re.escape(normalized_prefix)}/{file_label}_(\d+)\.{re.escape(ext)}$"
446
+ match = re.match(pattern, key)
447
+ if not match:
448
+ return None
449
+ return int(match.group(1))
450
+
451
+
452
+ def _fetch_spaces_max_pair_number() -> int:
453
+ if not spaces_enabled or spaces_client is None:
454
+ return 0
455
+
456
+ max_number = 0
457
+ for prefix, label, ext in (
458
+ (SPACES_SOURCE_PREFIX, "source", "jpg"),
459
+ (SPACES_RESULTS_PREFIX, "result", "png"),
460
+ ):
461
+ continuation_token = None
462
+ while True:
463
+ kwargs = {
464
+ "Bucket": SPACES_BUCKET,
465
+ "Prefix": f"{prefix}/",
466
+ "MaxKeys": 1000,
467
+ }
468
+ if continuation_token:
469
+ kwargs["ContinuationToken"] = continuation_token
470
+ response = spaces_client.list_objects_v2(**kwargs)
471
+ for item in response.get("Contents", []):
472
+ key = item.get("Key", "")
473
+ pair_number = _extract_pair_number_from_key(key, prefix, label, ext)
474
+ if pair_number is not None:
475
+ max_number = max(max_number, pair_number)
476
+ if not response.get("IsTruncated"):
477
+ break
478
+ continuation_token = response.get("NextContinuationToken")
479
+ return max_number
480
+
481
+
482
+ def _next_spaces_pair_number() -> int:
483
+ global spaces_pair_counter
484
+
485
+ if not spaces_enabled or spaces_client is None:
486
+ raise RuntimeError("DigitalOcean Spaces is not configured")
487
+
488
+ with spaces_counter_lock:
489
+ if spaces_pair_counter is None:
490
+ spaces_pair_counter = _fetch_spaces_max_pair_number()
491
+ spaces_pair_counter += 1
492
+ return spaces_pair_counter
493
+
494
+
495
+ def _upload_bytes_to_spaces(data: bytes, key: str, content_type: str) -> None:
496
+ if not spaces_enabled or spaces_client is None:
497
+ return
498
+ spaces_client.put_object(
499
+ Bucket=SPACES_BUCKET,
500
+ Key=key,
501
+ Body=data,
502
+ ContentType=content_type,
503
+ )
504
+
505
+
506
+ def _mirror_source_result_to_spaces(
507
+ source_image: Image.Image,
508
+ result_image: Image.Image,
509
+ ) -> Optional[Dict[str, str]]:
510
+ if not spaces_enabled or spaces_client is None:
511
+ return None
512
+
513
+ pair_number = _next_spaces_pair_number()
514
+ source_key = f"{SPACES_SOURCE_PREFIX}/source_{pair_number}.jpg"
515
+ result_key = f"{SPACES_RESULTS_PREFIX}/result_{pair_number}.png"
516
+
517
+ source_bytes = _pil_to_jpeg_bytes(source_image)
518
+ result_bytes = _pil_to_png_bytes(result_image)
519
+
520
+ _upload_bytes_to_spaces(source_bytes, source_key, "image/jpeg")
521
+ _upload_bytes_to_spaces(result_bytes, result_key, "image/png")
522
+ log.info(
523
+ "Mirrored source/result to Spaces: source_key=%s result_key=%s",
524
+ source_key,
525
+ result_key,
526
+ )
527
+ return {
528
+ "source_key": source_key,
529
+ "result_key": result_key,
530
+ "pair_number": str(pair_number),
531
+ }
532
+
533
+
534
  def _build_ai_edit_daily_count(
535
  existing: Optional[List[Dict[str, object]]],
536
  today: date,
 
965
  error_msg = None
966
  output_name = None
967
  compressed_url = None
968
+ spaces_info = None
969
 
970
  try:
971
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
 
992
  output_name = f"output_{uuid.uuid4().hex}.png"
993
  output_path = os.path.join(OUTPUT_DIR, output_name)
994
 
995
+ result_image = Image.fromarray(result)
996
+ result_image.save(output_path, "PNG", optimize=False, compress_level=1)
997
+ try:
998
+ spaces_info = _mirror_source_result_to_spaces(img_rgba, result_image)
999
+ except Exception as spaces_err:
1000
+ log.error("Failed to mirror /inpaint images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1001
 
1002
  # Create compressed version
1003
  compressed_name = f"compressed_{output_name.replace('.png', '.jpg')}"
 
1018
  response = {"result": output_name}
1019
  if compressed_url:
1020
  response["Compressed_Image_URL"] = compressed_url
1021
+ if spaces_info:
1022
+ response["source_space_key"] = spaces_info["source_key"]
1023
+ response["result_space_key"] = spaces_info["result_key"]
1024
  return response
1025
 
1026
  except Exception as e:
 
1056
 
1057
  if error_msg:
1058
  log_doc["error"] = error_msg
1059
+ if spaces_info:
1060
+ log_doc["source_space_key"] = spaces_info["source_key"]
1061
+ log_doc["result_space_key"] = spaces_info["result_key"]
1062
 
1063
  # if mongo_logs is not None:
1064
  # try:
 
1122
  status = "success"
1123
  error_msg = None
1124
  result_name = None
1125
+ spaces_info = None
1126
 
1127
  try:
1128
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
 
1147
  )
1148
  result_name = f"output_{uuid.uuid4().hex}.png"
1149
  result_path = os.path.join(OUTPUT_DIR, result_name)
1150
+ result_image = Image.fromarray(result)
1151
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1152
+ try:
1153
+ spaces_info = _mirror_source_result_to_spaces(img_rgba, result_image)
1154
+ except Exception as spaces_err:
1155
+ log.error("Failed to mirror /inpaint-url images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1156
 
1157
  url = str(request.url_for("download_file", filename=result_name))
1158
  logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
1159
  log_media_click(req.user_id, category_id, req.appname)
1160
+ response = {"result": result_name, "url": url}
1161
+ if spaces_info:
1162
+ response["source_space_key"] = spaces_info["source_key"]
1163
+ response["result_space_key"] = spaces_info["result_key"]
1164
+ return response
1165
  except Exception as e:
1166
  status = "fail"
1167
  error_msg = str(e)
 
1184
  log_doc["appname"] = req.appname
1185
  if error_msg:
1186
  log_doc["error"] = error_msg
1187
+ if spaces_info:
1188
+ log_doc["source_space_key"] = spaces_info["source_key"]
1189
+ log_doc["result_space_key"] = spaces_info["result_key"]
1190
  if mongo_logs is not None:
1191
  try:
1192
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
 
1227
  status = "success"
1228
  error_msg = None
1229
  result_name = None
1230
+ spaces_info = None
1231
 
1232
  try:
1233
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
 
1246
  result = np.array(img.convert("RGB"))
1247
  result_name = f"output_{uuid.uuid4().hex}.png"
1248
  result_path = os.path.join(OUTPUT_DIR, result_name)
1249
+ result_image = Image.fromarray(result)
1250
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1251
+ try:
1252
+ spaces_info = _mirror_source_result_to_spaces(img, result_image)
1253
+ except Exception as spaces_err:
1254
+ log.error("Failed to mirror /inpaint-multipart passthrough images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1255
 
1256
  url: Optional[str] = None
1257
  try:
 
1267
  resp: Dict[str, str] = {"result": result_name}
1268
  if url:
1269
  resp["url"] = url
1270
+ if spaces_info:
1271
+ resp["source_space_key"] = spaces_info["source_key"]
1272
+ resp["result_space_key"] = spaces_info["result_key"]
1273
  log_media_click(user_id, final_category_id, appname)
1274
  return resp
1275
 
 
1323
  result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
1324
  result_name = f"output_{uuid.uuid4().hex}.png"
1325
  result_path = os.path.join(OUTPUT_DIR, result_name)
1326
+ result_image = Image.fromarray(result)
1327
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1328
+ try:
1329
+ spaces_info = _mirror_source_result_to_spaces(img if img else m, result_image)
1330
+ except Exception as spaces_err:
1331
+ log.error("Failed to mirror /inpaint-multipart fallback images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1332
+ response = {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
1333
+ if spaces_info:
1334
+ response["source_space_key"] = spaces_info["source_key"]
1335
+ response["result_space_key"] = spaces_info["result_key"]
1336
+ return response
1337
 
1338
  # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1339
  # Encode in RGBA format for process_inpaint
 
1364
  )
1365
  result_name = f"output_{uuid.uuid4().hex}.png"
1366
  result_path = os.path.join(OUTPUT_DIR, result_name)
1367
+ result_image = Image.fromarray(result)
1368
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1369
+ try:
1370
+ spaces_info = _mirror_source_result_to_spaces(img, result_image)
1371
+ except Exception as spaces_err:
1372
+ log.error("Failed to mirror /inpaint-multipart images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1373
 
1374
  url: Optional[str] = None
1375
  try:
 
1385
  resp: Dict[str, str] = {"result": result_name}
1386
  if url:
1387
  resp["url"] = url
1388
+ if spaces_info:
1389
+ resp["source_space_key"] = spaces_info["source_key"]
1390
+ resp["result_space_key"] = spaces_info["result_key"]
1391
  log_media_click(user_id, final_category_id, appname)
1392
  return resp
1393
  except Exception as e:
 
1411
  log_doc["appname"] = appname
1412
  if error_msg:
1413
  log_doc["error"] = error_msg
1414
+ if spaces_info:
1415
+ log_doc["source_space_key"] = spaces_info["source_key"]
1416
+ log_doc["result_space_key"] = spaces_info["result_key"]
1417
  if mongo_logs is not None:
1418
  try:
1419
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
 
1455
  status = "success"
1456
  error_msg = None
1457
  result_name = None
1458
+ spaces_info = None
1459
 
1460
  try:
1461
  # Handle appname="collage-maker": get category_id from collage-maker if not provided
 
1501
  result = np.array(img.convert("RGB"))
1502
  result_name = f"output_{uuid.uuid4().hex}.png"
1503
  result_path = os.path.join(OUTPUT_DIR, result_name)
1504
+ result_image = Image.fromarray(result)
1505
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1506
+ try:
1507
+ spaces_info = _mirror_source_result_to_spaces(img, result_image)
1508
+ except Exception as spaces_err:
1509
+ log.error("Failed to mirror /remove-pink fallback images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1510
+ response = {
1511
  "result": result_name,
1512
  "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
1513
  }
1514
+ if spaces_info:
1515
+ response["source_space_key"] = spaces_info["source_key"]
1516
+ response["result_space_key"] = spaces_info["result_key"]
1517
+ return response
1518
 
1519
  # Create binary mask: Pink pixels → white (255), Everything else → black (0)
1520
  # Encode in RGBA format that process_inpaint expects
 
1551
  log.info(f"Inpainting complete, result shape: {result.shape}")
1552
  result_name = f"output_{uuid.uuid4().hex}.png"
1553
  result_path = os.path.join(OUTPUT_DIR, result_name)
1554
+ result_image = Image.fromarray(result)
1555
+ result_image.save(result_path, "PNG", optimize=False, compress_level=1)
1556
+ try:
1557
+ spaces_info = _mirror_source_result_to_spaces(img, result_image)
1558
+ except Exception as spaces_err:
1559
+ log.error("Failed to mirror /remove-pink images to DigitalOcean Spaces: %s", spaces_err, exc_info=True)
1560
 
1561
  url: Optional[str] = None
1562
  try:
 
1575
  resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
1576
  if url:
1577
  resp["url"] = url
1578
+ if spaces_info:
1579
+ resp["source_space_key"] = spaces_info["source_key"]
1580
+ resp["result_space_key"] = spaces_info["result_key"]
1581
  log_media_click(user_id, final_category_id, appname)
1582
  return resp
1583
  except Exception as e:
 
1601
  log_doc["appname"] = appname
1602
  if error_msg:
1603
  log_doc["error"] = error_msg
1604
+ if spaces_info:
1605
+ log_doc["source_space_key"] = spaces_info["source_key"]
1606
+ log_doc["result_space_key"] = spaces_info["result_key"]
1607
  if mongo_logs is not None:
1608
  try:
1609
  log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
 
1646
  @app.get("/logs")
1647
  def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
1648
  return JSONResponse(content=logs)
1649
+
1650
+ # import os
1651
+ # import uuid
1652
+ # import shutil
1653
+ # import re
1654
+ # from datetime import datetime, timedelta, date
1655
+ # from io import BytesIO
1656
+ # from typing import Dict, List, Optional,Any
1657
+ # import numpy as np
1658
+ # from fastapi import (
1659
+ # FastAPI,
1660
+ # UploadFile,
1661
+ # File,
1662
+ # HTTPException,
1663
+ # Depends,
1664
+ # Header,
1665
+ # Request,
1666
+ # Form,
1667
+ # )
1668
+ # from fastapi.responses import FileResponse, JSONResponse
1669
+ # from pydantic import BaseModel
1670
+ # from PIL import Image, UnidentifiedImageError
1671
+ # import cv2
1672
+ # import logging
1673
+ # from gridfs import GridFS
1674
+ # from gridfs.errors import NoFile
1675
+
1676
+ # from bson import ObjectId
1677
+ # from pymongo import MongoClient
1678
+ # import time
1679
+
1680
+ # # Load environment variables from .env if present
1681
+ # try:
1682
+ # from dotenv import load_dotenv
1683
+
1684
+ # load_dotenv()
1685
+ # except Exception:
1686
+ # pass
1687
+
1688
+ # logging.basicConfig(level=logging.INFO)
1689
+ # log = logging.getLogger("api")
1690
+
1691
+ # from src.core import process_inpaint
1692
+
1693
+ # # Directories (use writable space on HF Spaces)
1694
+ # BASE_DIR = os.environ.get("DATA_DIR", "/data")
1695
+ # if not os.path.isdir(BASE_DIR):
1696
+ # # Fallback to /tmp if /data not available
1697
+ # BASE_DIR = "/tmp"
1698
+
1699
+ # UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
1700
+ # OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
1701
+
1702
+ # os.makedirs(UPLOAD_DIR, exist_ok=True)
1703
+ # os.makedirs(OUTPUT_DIR, exist_ok=True)
1704
+
1705
+ # # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
1706
+ # ENV_TOKEN = os.environ.get("API_TOKEN")
1707
+
1708
+ # app = FastAPI(title="Photo Object Removal API", version="1.0.0")
1709
+
1710
+ # # In-memory stores
1711
+ # file_store: Dict[str, Dict[str, str]] = {}
1712
+ # logs: List[Dict[str, str]] = []
1713
+
1714
+ # MONGO_URI = os.environ.get("MONGO_URI") or os.environ.get("MONGODB_URI")
1715
+ # mongo_client = None
1716
+ # mongo_db = None
1717
+ # mongo_logs = None
1718
+ # grid_fs = None
1719
+
1720
+ # if MONGO_URI:
1721
+ # try:
1722
+ # mongo_client = MongoClient(MONGO_URI)
1723
+ # # Try to get database from connection string first
1724
+ # try:
1725
+ # mongo_db = mongo_client.get_default_database()
1726
+ # log.info("Using database from connection string: %s", mongo_db.name)
1727
+ # except Exception as db_err:
1728
+ # mongo_db = None
1729
+ # log.warning("Could not extract database from connection string: %s", db_err)
1730
+
1731
+ # # Fallback to 'object_remover' if no database in connection string
1732
+ # if mongo_db is None:
1733
+ # mongo_db = mongo_client["object_remover"]
1734
+ # log.info("Using default database: object_remover")
1735
+
1736
+ # mongo_logs = mongo_db["api_logs"]
1737
+ # grid_fs = GridFS(mongo_db)
1738
+ # log.info("MongoDB connection initialized successfully - Database: %s, Collection: %s", mongo_db.name, mongo_logs.name)
1739
+ # except Exception as err:
1740
+ # log.error("Failed to initialize MongoDB connection: %s", err, exc_info=True)
1741
+ # log.warning("GridFS operations will be disabled. Set MONGO_URI or MONGODB_URI environment variable.")
1742
+ # else:
1743
+ # log.warning("MONGO_URI not set. GridFS operations will be disabled. Upload endpoints will not work.")
1744
+ # # if MONGO_URI:
1745
+ # # try:
1746
+ # # mongo_client = MongoClient(MONGO_URI)
1747
+
1748
+ # # # 🔥 FORCE DB + COLLECTION
1749
+ # # mongo_db = mongo_client["logs"]
1750
+ # # mongo_logs = mongo_db["objectRemover"]
1751
+
1752
+ # # log.info("MongoDB initialized → logs/objectRemover")
1753
+ # # except Exception as err:
1754
+ # # log.error("Failed to initialize MongoDB connection: %s", err, exc_info=True)
1755
+ # # mongo_logs = None
1756
+ # # else:
1757
+ # # log.warning("MONGO_URI not set. Logging disabled.")
1758
+ # # ✅ NEW: Separate Mongo for API Logs
1759
+ # API_LOGS_MONGO_URI = os.environ.get("API_LOGS_MONGODB_URL")
1760
+
1761
+ # api_logs_client = None
1762
+ # api_logs_db = None
1763
+ # api_logs_collection = None
1764
+
1765
+ # if API_LOGS_MONGO_URI:
1766
+ # try:
1767
+ # api_logs_client = MongoClient(API_LOGS_MONGO_URI)
1768
+ # api_logs_db = api_logs_client["logs"] # 🔥 logs database
1769
+ # api_logs_collection = api_logs_db["objectRemover"] # 🔥 objectRemover collection
1770
+ # log.info("API Logs Mongo initialized → logs/objectRemover")
1771
+ # except Exception as e:
1772
+ # log.error("Failed to initialize API Logs MongoDB: %s", e)
1773
+ # api_logs_collection = None
1774
+ # else:
1775
+ # log.warning("API_LOGS_MONGODB_URL not set. API logging disabled.")
1776
+
1777
+ # ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
1778
+ # DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
1779
+ # admin_media_clicks = None
1780
+
1781
+ # # Collage-maker MongoDB configuration
1782
+ # COLLAGE_MAKER_MONGO_URI = os.environ.get("MONGODB_COLLAGE_MAKER")
1783
+ # COLLAGE_MAKER_DB_NAME = os.environ.get("MONGODB_COLLAGE_MAKER_DB_NAME", "collage-maker")
1784
+ # COLLAGE_MAKER_ADMIN_DB_NAME = os.environ.get("MONGODB_COLLAGE_MAKER_ADMIN_DB_NAME", "adminPanel")
1785
+ # collage_maker_client = None
1786
+ # collage_maker_db = None
1787
+ # collage_maker_admin_db = None
1788
+ # collage_maker_media_clicks = None
1789
+ # collage_maker_categories = None
1790
+
1791
+ # # AI-Enhancer MongoDB configuration
1792
+ # AI_ENHANCER_MONGO_URI = os.environ.get("MONGODB_AI_ENHANCER")
1793
+ # AI_ENHANCER_DB_NAME = os.environ.get("MONGODB_AI_ENHANCER_DB_NAME", "ai-enhancer")
1794
+ # AI_ENHANCER_ADMIN_DB_NAME = os.environ.get("MONGODB_AI_ENHANCER_ADMIN_DB_NAME", "test")
1795
+ # ai_enhancer_client = None
1796
+ # ai_enhancer_db = None
1797
+ # ai_enhancer_admin_db = None
1798
+ # ai_enhancer_media_clicks = None
1799
+
1800
+
1801
+ # def get_collage_maker_client() -> Optional[MongoClient]:
1802
+ # """Get collage-maker MongoDB client."""
1803
+ # global collage_maker_client
1804
+ # if collage_maker_client is None and COLLAGE_MAKER_MONGO_URI:
1805
+ # try:
1806
+ # collage_maker_client = MongoClient(COLLAGE_MAKER_MONGO_URI)
1807
+ # log.info("Collage-maker MongoDB client initialized")
1808
+ # except Exception as err:
1809
+ # log.error("Failed to initialize collage-maker MongoDB client: %s", err)
1810
+ # collage_maker_client = None
1811
+ # return collage_maker_client
1812
+
1813
+
1814
+ # def get_collage_maker_database() -> Optional[Any]:
1815
+ # """Get collage-maker database instance."""
1816
+ # global collage_maker_db
1817
+ # client = get_collage_maker_client()
1818
+ # if client is None:
1819
+ # return None
1820
+ # if collage_maker_db is None:
1821
+ # try:
1822
+ # collage_maker_db = client[COLLAGE_MAKER_DB_NAME]
1823
+ # log.info("Collage-maker database initialized: %s", COLLAGE_MAKER_DB_NAME)
1824
+ # except Exception as err:
1825
+ # log.error("Failed to get collage-maker database: %s", err)
1826
+ # collage_maker_db = None
1827
+ # return collage_maker_db
1828
+
1829
+
1830
+ # def _init_collage_maker_mongo() -> None:
1831
+ # """Initialize collage-maker MongoDB connections."""
1832
+ # global collage_maker_admin_db, collage_maker_media_clicks, collage_maker_categories
1833
+ # client = get_collage_maker_client()
1834
+ # if client is None:
1835
+ # log.info("Collage-maker Mongo URI not provided; collage-maker features disabled")
1836
+ # return
1837
+ # try:
1838
+ # collage_maker_admin_db = client[COLLAGE_MAKER_ADMIN_DB_NAME]
1839
+ # collage_maker_media_clicks = collage_maker_admin_db["media_clicks"]
1840
+ # collage_maker_categories = collage_maker_admin_db["categories"]
1841
+ # log.info(
1842
+ # "Collage-maker admin initialized: db=%s, media_clicks=%s, categories=%s",
1843
+ # COLLAGE_MAKER_ADMIN_DB_NAME,
1844
+ # collage_maker_media_clicks.name,
1845
+ # collage_maker_categories.name,
1846
+ # )
1847
+ # except Exception as err:
1848
+ # log.error("Failed to init collage-maker admin Mongo: %s", err)
1849
+ # collage_maker_admin_db = None
1850
+ # collage_maker_media_clicks = None
1851
+ # collage_maker_categories = None
1852
+
1853
+
1854
+ # _init_collage_maker_mongo()
1855
+
1856
+
1857
+ # def get_ai_enhancer_client() -> Optional[MongoClient]:
1858
+ # """Get AI-Enhancer MongoDB client."""
1859
+ # global ai_enhancer_client
1860
+ # if ai_enhancer_client is None and AI_ENHANCER_MONGO_URI:
1861
+ # try:
1862
+ # ai_enhancer_client = MongoClient(AI_ENHANCER_MONGO_URI)
1863
+ # log.info("AI-Enhancer MongoDB client initialized")
1864
+ # except Exception as err:
1865
+ # log.error("Failed to initialize AI-Enhancer MongoDB client: %s", err)
1866
+ # ai_enhancer_client = None
1867
+ # return ai_enhancer_client
1868
+
1869
+
1870
+ # def get_ai_enhancer_database() -> Optional[Any]:
1871
+ # """Get AI-Enhancer database instance."""
1872
+ # global ai_enhancer_db
1873
+ # client = get_ai_enhancer_client()
1874
+ # if client is None:
1875
+ # return None
1876
+ # if ai_enhancer_db is None:
1877
+ # try:
1878
+ # ai_enhancer_db = client[AI_ENHANCER_DB_NAME]
1879
+ # log.info("AI-Enhancer database initialized: %s", AI_ENHANCER_DB_NAME)
1880
+ # except Exception as err:
1881
+ # log.error("Failed to get AI-Enhancer database: %s", err)
1882
+ # ai_enhancer_db = None
1883
+ # return ai_enhancer_db
1884
+
1885
+
1886
+ # def _init_ai_enhancer_mongo() -> None:
1887
+ # """Initialize AI-Enhancer MongoDB connections."""
1888
+ # global ai_enhancer_admin_db, ai_enhancer_media_clicks
1889
+ # client = get_ai_enhancer_client()
1890
+ # if client is None:
1891
+ # log.info("AI-Enhancer Mongo URI not provided; AI-Enhancer features disabled")
1892
+ # return
1893
+ # try:
1894
+ # ai_enhancer_admin_db = client[AI_ENHANCER_ADMIN_DB_NAME]
1895
+ # ai_enhancer_media_clicks = ai_enhancer_admin_db["media_clicks"]
1896
+ # log.info(
1897
+ # "AI-Enhancer admin initialized: db=%s, media_clicks=%s",
1898
+ # AI_ENHANCER_ADMIN_DB_NAME,
1899
+ # ai_enhancer_media_clicks.name,
1900
+ # )
1901
+ # except Exception as err:
1902
+ # log.error("Failed to init AI-Enhancer admin Mongo: %s", err)
1903
+ # ai_enhancer_admin_db = None
1904
+ # ai_enhancer_media_clicks = None
1905
+
1906
+
1907
+ # _init_ai_enhancer_mongo()
1908
+
1909
+
1910
+ # def get_category_id_from_collage_maker() -> Optional[str]:
1911
+ # """Query category ID from collage-maker categories collection."""
1912
+ # if collage_maker_categories is None:
1913
+ # log.warning("Collage-maker categories collection not initialized")
1914
+ # return None
1915
+ # try:
1916
+ # # Query the categories collection - you may need to adjust the query based on your schema
1917
+ # # This assumes there's a default category or we get the first one
1918
+ # category_doc = collage_maker_categories.find_one()
1919
+ # if category_doc:
1920
+ # category_id = str(category_doc.get("_id", ""))
1921
+ # log.info("Found category ID from collage-maker: %s", category_id)
1922
+ # return category_id
1923
+ # else:
1924
+ # log.warning("No categories found in collage-maker collection")
1925
+ # return None
1926
+ # except Exception as err:
1927
+ # log.error("Failed to query collage-maker categories: %s", err)
1928
+ # return None
1929
+
1930
+
1931
+ # def _init_admin_mongo() -> None:
1932
+ # global admin_media_clicks
1933
+ # if not ADMIN_MONGO_URI:
1934
+ # log.info("Admin Mongo URI not provided; media click logging disabled")
1935
+ # return
1936
+ # try:
1937
+ # admin_client = MongoClient(ADMIN_MONGO_URI)
1938
+ # # get_default_database() extracts database from connection string (e.g., /adminPanel)
1939
+ # try:
1940
+ # admin_db = admin_client.get_default_database()
1941
+ # except Exception as db_err:
1942
+ # admin_db = None
1943
+ # log.warning("Admin Mongo URI has no default DB; error=%s", db_err)
1944
+ # if admin_db is None:
1945
+ # # Fallback to provided default for this app
1946
+ # admin_db = admin_client["object_remover"]
1947
+ # log.warning("No database in connection string, defaulting to 'object_remover'")
1948
+
1949
+ # admin_media_clicks = admin_db["media_clicks"]
1950
+ # log.info(
1951
+ # "Admin media click logging initialized: db=%s collection=%s",
1952
+ # admin_db.name,
1953
+ # admin_media_clicks.name,
1954
+ # )
1955
+ # try:
1956
+ # admin_media_clicks.drop_index("user_id_1_header_1_media_id_1")
1957
+ # log.info("Dropped legacy index user_id_1_header_1_media_id_1")
1958
+ # except Exception as idx_err:
1959
+ # # Index drop failure is non-critical (often permission issue)
1960
+ # if "Unauthorized" not in str(idx_err):
1961
+ # log.info("Skipping legacy index drop: %s", idx_err)
1962
+ # except Exception as err:
1963
+ # log.error("Failed to init admin Mongo client: %s", err)
1964
+ # admin_media_clicks = None
1965
+
1966
+
1967
+ # _init_admin_mongo()
1968
+
1969
+
1970
+ # def _admin_logging_status() -> Dict[str, object]:
1971
+ # if admin_media_clicks is None:
1972
+ # return {
1973
+ # "enabled": False,
1974
+ # "db": None,
1975
+ # "collection": None,
1976
+ # }
1977
+ # return {
1978
+ # "enabled": True,
1979
+ # "db": admin_media_clicks.database.name,
1980
+ # "collection": admin_media_clicks.name,
1981
+ # }
1982
+
1983
+
1984
+ # def _save_upload_to_gridfs(upload: UploadFile, file_type: str) -> str:
1985
+ # """Store an uploaded file into GridFS and return its ObjectId string."""
1986
+ # if grid_fs is None:
1987
+ # raise HTTPException(
1988
+ # status_code=503,
1989
+ # detail="MongoDB/GridFS not configured. Set MONGO_URI or MONGODB_URI environment variable."
1990
+ # )
1991
+ # data = upload.file.read()
1992
+ # if not data:
1993
+ # raise HTTPException(status_code=400, detail=f"{file_type} file is empty")
1994
+ # oid = grid_fs.put(
1995
+ # data,
1996
+ # filename=upload.filename or f"{file_type}.bin",
1997
+ # contentType=upload.content_type,
1998
+ # metadata={"type": file_type},
1999
+ # )
2000
+ # return str(oid)
2001
+
2002
+
2003
+ # def _read_gridfs_bytes(file_id: str, expected_type: str) -> bytes:
2004
+ # """Fetch raw bytes from GridFS and validate the stored type metadata."""
2005
+ # if grid_fs is None:
2006
+ # raise HTTPException(
2007
+ # status_code=503,
2008
+ # detail="MongoDB/GridFS not configured. Set MONGO_URI or MONGODB_URI environment variable."
2009
+ # )
2010
+ # try:
2011
+ # oid = ObjectId(file_id)
2012
+ # except Exception:
2013
+ # raise HTTPException(status_code=404, detail=f"{expected_type}_id invalid")
2014
+
2015
+ # try:
2016
+ # grid_out = grid_fs.get(oid)
2017
+ # except NoFile:
2018
+ # raise HTTPException(status_code=404, detail=f"{expected_type}_id not found")
2019
+
2020
+ # meta = grid_out.metadata or {}
2021
+ # stored_type = meta.get("type")
2022
+ # if stored_type and stored_type != expected_type:
2023
+ # raise HTTPException(status_code=404, detail=f"{expected_type}_id not found")
2024
+
2025
+ # return grid_out.read()
2026
+
2027
+
2028
+ # def _load_rgba_image_from_gridfs(file_id: str, expected_type: str) -> Image.Image:
2029
+ # """Load an image from GridFS and convert to RGBA."""
2030
+ # data = _read_gridfs_bytes(file_id, expected_type)
2031
+ # try:
2032
+ # img = Image.open(BytesIO(data))
2033
+ # except UnidentifiedImageError:
2034
+ # raise HTTPException(status_code=422, detail=f"{expected_type} is not a valid image")
2035
+ # return img.convert("RGBA")
2036
+
2037
+
2038
+ # def _build_ai_edit_daily_count(
2039
+ # existing: Optional[List[Dict[str, object]]],
2040
+ # today: date,
2041
+ # ) -> List[Dict[str, object]]:
2042
+ # """
2043
+ # Build / extend the ai_edit_daily_count array with the following rules:
2044
+
2045
+ # - Case A (no existing data): return [{date: today, count: 1}]
2046
+ # - Case B (today already recorded): return list unchanged
2047
+ # - Case C (gap in days): fill missing days with count=0 and append today with count=1
2048
+
2049
+ # Additionally, the returned list is capped to the most recent 32 entries.
2050
+
2051
+ # The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
2052
+ # """
2053
+
2054
+ # def _to_date_only(value: object) -> date:
2055
+ # if isinstance(value, datetime):
2056
+ # return value.date()
2057
+ # if isinstance(value, date):
2058
+ # return value
2059
+ # # Fallback: try parsing ISO string "YYYY-MM-DD" or full datetime
2060
+ # try:
2061
+ # text = str(value)
2062
+ # if len(text) == 10:
2063
+ # return datetime.strptime(text, "%Y-%m-%d").date()
2064
+ # return datetime.fromisoformat(text).date()
2065
+ # except Exception:
2066
+ # # If parsing fails, just treat as today to avoid crashing
2067
+ # return today
2068
+
2069
+ # # Case A: first ever use (no array yet)
2070
+ # if not existing:
2071
+ # return [
2072
+ # {
2073
+ # "date": datetime(today.year, today.month, today.day),
2074
+ # "count": 1,
2075
+ # }
2076
+ # ]
2077
+
2078
+ # # Work on a shallow copy so we don't mutate original in-place
2079
+ # result: List[Dict[str, object]] = list(existing)
2080
+
2081
+ # last_entry = result[-1] if result else None
2082
+ # if not last_entry or "date" not in last_entry:
2083
+ # # If structure is unexpected, re-initialize safely
2084
+ # return [
2085
+ # {
2086
+ # "date": datetime(today.year, today.month, today.day),
2087
+ # "count": 1,
2088
+ # }
2089
+ # ]
2090
+
2091
+ # last_date = _to_date_only(last_entry["date"])
2092
+
2093
+ # # If somehow the last stored date is in the future, do nothing to avoid corrupting history
2094
+ # if last_date > today:
2095
+ # return result
2096
+
2097
+ # # Case B: today's date already present as the last entry → unchanged
2098
+ # if last_date == today:
2099
+ # return result
2100
+
2101
+ # # Case C: there is a gap, fill missing days with count=0 and append today with count=1
2102
+ # cursor = last_date + timedelta(days=1)
2103
+ # while cursor < today:
2104
+ # result.append(
2105
+ # {
2106
+ # "date": datetime(cursor.year, cursor.month, cursor.day),
2107
+ # "count": 0,
2108
+ # }
2109
+ # )
2110
+ # cursor += timedelta(days=1)
2111
+
2112
+ # # Finally add today's presence indicator
2113
+ # result.append(
2114
+ # {
2115
+ # "date": datetime(today.year, today.month, today.day),
2116
+ # "count": 1,
2117
+ # }
2118
+ # )
2119
+
2120
+ # # Sort by date ascending (older dates first) to guarantee stable ordering:
2121
+ # # [oldest, ..., newest]
2122
+ # try:
2123
+ # result.sort(key=lambda entry: _to_date_only(entry.get("date")))
2124
+ # except Exception:
2125
+ # # If anything goes wrong during sort, fall back to current ordering
2126
+ # pass
2127
+
2128
+ # # Enforce 32-entry limit (keep the most recent 32 days)
2129
+ # if len(result) > 32:
2130
+ # result = result[-32:]
2131
+
2132
+ # return result
2133
+
2134
+ # def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
2135
+ # if not ENV_TOKEN:
2136
+ # return
2137
+ # if authorization is None or not authorization.lower().startswith("bearer "):
2138
+ # raise HTTPException(status_code=401, detail="Unauthorized")
2139
+ # token = authorization.split(" ", 1)[1]
2140
+ # if token != ENV_TOKEN:
2141
+ # raise HTTPException(status_code=403, detail="Forbidden")
2142
+
2143
+
2144
+ # class InpaintRequest(BaseModel):
2145
+ # image_id: str
2146
+ # mask_id: str
2147
+ # invert_mask: bool = True # True => selected/painted area is removed
2148
+ # passthrough: bool = False # If True, return the original image unchanged
2149
+ # prompt: Optional[str] = None # Optional: describe what to remove
2150
+ # user_id: Optional[str] = None
2151
+ # category_id: Optional[str] = None
2152
+ # appname: Optional[str] = None # Optional: app name (e.g., "collage-maker")
2153
+
2154
+
2155
+ # class SimpleRemoveRequest(BaseModel):
2156
+ # image_id: str # Image with pink/magenta segments to remove
2157
+
2158
+
2159
+ # def _coerce_object_id(value: Optional[str]) -> ObjectId:
2160
+ # if value is None:
2161
+ # return ObjectId()
2162
+ # value_str = str(value).strip()
2163
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", value_str):
2164
+ # return ObjectId(value_str)
2165
+ # if value_str.isdigit():
2166
+ # hex_str = format(int(value_str), "x")
2167
+ # if len(hex_str) > 24:
2168
+ # hex_str = hex_str[-24:]
2169
+ # hex_str = hex_str.rjust(24, "0")
2170
+ # return ObjectId(hex_str)
2171
+ # return ObjectId()
2172
+
2173
+
2174
+ # def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
2175
+ # raw = category_id or DEFAULT_CATEGORY_ID
2176
+ # raw_str = str(raw).strip()
2177
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", raw_str):
2178
+ # return ObjectId(raw_str)
2179
+ # return _coerce_object_id(raw_str)
2180
+
2181
+
2182
+ # def log_media_click(user_id: Optional[str], category_id: Optional[str], appname: Optional[str] = None) -> None:
2183
+ # """Log to admin media_clicks collection only if user_id is provided.
2184
+
2185
+ # If appname='collage-maker', logs to collage-maker MongoDB instead of regular admin MongoDB.
2186
+ # If appname='AI-Enhancer' (case-insensitive), logs to AI-Enhancer MongoDB.
2187
+ # """
2188
+ # # Determine which media_clicks collection to use
2189
+ # target_media_clicks = None
2190
+ # appname_lower = appname.lower() if appname else None
2191
+
2192
+ # if appname_lower == "collage-maker":
2193
+ # target_media_clicks = collage_maker_media_clicks
2194
+ # if target_media_clicks is None:
2195
+ # log.warning("Collage-maker media_clicks not initialized, skipping log")
2196
+ # return
2197
+ # elif appname_lower == "ai-enhancer":
2198
+ # target_media_clicks = ai_enhancer_media_clicks
2199
+ # if target_media_clicks is None:
2200
+ # log.warning("AI-Enhancer media_clicks not initialized, skipping log")
2201
+ # return
2202
+ # else:
2203
+ # target_media_clicks = admin_media_clicks
2204
+ # if target_media_clicks is None:
2205
+ # return
2206
+
2207
+ # # Only log if user_id is provided (not None/empty)
2208
+ # if not user_id or not user_id.strip():
2209
+ # return
2210
+ # try:
2211
+ # user_obj = _coerce_object_id(user_id)
2212
+ # category_obj = _coerce_category_id(category_id)
2213
+ # now = datetime.utcnow()
2214
+ # today = now.date()
2215
+
2216
+ # doc = target_media_clicks.find_one({"userId": user_obj})
2217
+ # if doc:
2218
+ # existing_daily = doc.get("ai_edit_daily_count")
2219
+ # updated_daily = _build_ai_edit_daily_count(existing_daily, today)
2220
+ # categories = doc.get("categories") or []
2221
+ # if any(cat.get("categoryId") == category_obj for cat in categories):
2222
+ # # Category exists: increment click_count and ai_edit_complete, update dates
2223
+ # target_media_clicks.update_one(
2224
+ # {"_id": doc["_id"], "categories.categoryId": category_obj},
2225
+ # {
2226
+ # "$inc": {
2227
+ # "categories.$.click_count": 1,
2228
+ # "ai_edit_complete": 1, # $inc handles missing fields (backward compatible)
2229
+ # },
2230
+ # "$set": {
2231
+ # "categories.$.lastClickedAt": now,
2232
+ # "updatedAt": now,
2233
+ # "ai_edit_last_date": now,
2234
+ # "ai_edit_daily_count": updated_daily,
2235
+ # },
2236
+ # },
2237
+ # )
2238
+ # else:
2239
+ # # New category to existing document: push category, increment ai_edit_complete
2240
+ # target_media_clicks.update_one(
2241
+ # {"_id": doc["_id"]},
2242
+ # {
2243
+ # "$push": {
2244
+ # "categories": {
2245
+ # "categoryId": category_obj,
2246
+ # "click_count": 1,
2247
+ # "lastClickedAt": now,
2248
+ # }
2249
+ # },
2250
+ # "$inc": {"ai_edit_complete": 1}, # $inc handles missing fields
2251
+ # "$set": {
2252
+ # "updatedAt": now,
2253
+ # "ai_edit_last_date": now,
2254
+ # "ai_edit_daily_count": updated_daily,
2255
+ # },
2256
+ # },
2257
+ # )
2258
+ # else:
2259
+ # # New user: create document with default ai_edit_complete=0, then increment to 1
2260
+ # daily_for_new = _build_ai_edit_daily_count(None, today)
2261
+ # target_media_clicks.update_one(
2262
+ # {"userId": user_obj},
2263
+ # {
2264
+ # "$setOnInsert": {
2265
+ # "userId": user_obj,
2266
+ # "categories": [
2267
+ # {
2268
+ # "categoryId": category_obj,
2269
+ # "click_count": 1,
2270
+ # "lastClickedAt": now,
2271
+ # }
2272
+ # ],
2273
+ # "createdAt": now,
2274
+ # "ai_edit_daily_count": daily_for_new,
2275
+ # },
2276
+ # "$inc": {"ai_edit_complete": 1}, # Increment to 1 on first use
2277
+ # "$set": {
2278
+ # "updatedAt": now,
2279
+ # "ai_edit_last_date": now,
2280
+ # },
2281
+ # },
2282
+ # upsert=True,
2283
+ # )
2284
+ # except Exception as err:
2285
+ # err_str = str(err)
2286
+ # appname_lower = appname.lower() if appname else None
2287
+ # if appname_lower == "collage-maker":
2288
+ # target_name = "collage-maker"
2289
+ # elif appname_lower == "ai-enhancer":
2290
+ # target_name = "AI-Enhancer"
2291
+ # else:
2292
+ # target_name = "admin"
2293
+ # if "Unauthorized" in err_str or "not authorized" in err_str.lower():
2294
+ # log.warning(
2295
+ # "%s media click logging failed (permissions): user lacks read/write on db=%s collection=%s. "
2296
+ # "Check MongoDB user permissions.",
2297
+ # target_name,
2298
+ # target_media_clicks.database.name,
2299
+ # target_media_clicks.name,
2300
+ # )
2301
+ # else:
2302
+ # log.warning("%s media click logging failed: %s", target_name, err)
2303
+
2304
+
2305
+ # @app.get("/")
2306
+ # def root() -> Dict[str, Any]:
2307
+ # return {
2308
+ # "success": True,
2309
+ # "message": "Object Remover API",
2310
+ # "data": {
2311
+ # "version": "1.0.0",
2312
+ # "product_name": "Beauty Camera - GlowCam AI Studio",
2313
+ # "released_by": "LogicGo Infotech"
2314
+ # }
2315
+ # }
2316
+
2317
+
2318
+
2319
+ # @app.get("/health")
2320
+ # def health() -> Dict[str, str]:
2321
+ # return {"status": "healthy"}
2322
+
2323
+
2324
+ # @app.get("/logging-status")
2325
+ # def logging_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
2326
+ # """Helper endpoint to verify admin media logging wiring (no secrets exposed)."""
2327
+ # return _admin_logging_status()
2328
+
2329
+
2330
+ # @app.get("/mongo-status")
2331
+ # def mongo_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
2332
+ # """Check MongoDB connection status and verify data storage."""
2333
+ # status = {
2334
+ # "mongo_configured": MONGO_URI is not None,
2335
+ # "mongo_connected": mongo_client is not None,
2336
+ # "database": mongo_db.name if mongo_db else None,
2337
+ # "collection": mongo_logs.name if mongo_logs else None,
2338
+ # "admin_logging": _admin_logging_status(),
2339
+ # }
2340
+
2341
+ # # Try to count documents in api_logs collection
2342
+ # if mongo_logs is not None:
2343
+ # try:
2344
+ # count = mongo_logs.count_documents({})
2345
+ # status["api_logs_count"] = count
2346
+ # # Get latest 5 documents
2347
+ # latest_docs = list(mongo_logs.find().sort("timestamp", -1).limit(5))
2348
+ # status["recent_logs"] = []
2349
+ # for doc in latest_docs:
2350
+ # doc_dict = {
2351
+ # "_id": str(doc.get("_id")),
2352
+ # "output_id": doc.get("output_id"),
2353
+ # "status": doc.get("status"),
2354
+ # "timestamp": doc.get("timestamp").isoformat() if isinstance(doc.get("timestamp"), datetime) else str(doc.get("timestamp")),
2355
+ # }
2356
+ # if "input_image_id" in doc:
2357
+ # doc_dict["input_image_id"] = doc.get("input_image_id")
2358
+ # if "input_mask_id" in doc:
2359
+ # doc_dict["input_mask_id"] = doc.get("input_mask_id")
2360
+ # if "error" in doc:
2361
+ # doc_dict["error"] = doc.get("error")
2362
+ # status["recent_logs"].append(doc_dict)
2363
+
2364
+ # # Get latest document for backward compatibility
2365
+ # if latest_docs:
2366
+ # latest = latest_docs[0]
2367
+ # status["latest_log"] = {
2368
+ # "_id": str(latest.get("_id")),
2369
+ # "output_id": latest.get("output_id"),
2370
+ # "status": latest.get("status"),
2371
+ # "timestamp": latest.get("timestamp").isoformat() if isinstance(latest.get("timestamp"), datetime) else str(latest.get("timestamp")),
2372
+ # }
2373
+ # except Exception as err:
2374
+ # status["api_logs_error"] = str(err)
2375
+ # log.error("Error querying MongoDB: %s", err, exc_info=True)
2376
+
2377
+ # return status
2378
+
2379
+
2380
+ # @app.post("/upload-image")
2381
+ # def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
2382
+ # file_id = _save_upload_to_gridfs(image, "image")
2383
+ # logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
2384
+ # return {"id": file_id, "filename": image.filename}
2385
+
2386
+
2387
+ # @app.post("/upload-mask")
2388
+ # def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
2389
+ # file_id = _save_upload_to_gridfs(mask, "mask")
2390
+ # logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
2391
+ # return {"id": file_id, "filename": mask.filename}
2392
+
2393
+
2394
+ # def _compress_image(image_path: str, output_path: str, quality: int = 85) -> None:
2395
+ # """
2396
+ # Compress an image to reduce file size.
2397
+ # Converts to JPEG format with specified quality to achieve smaller file size.
2398
+ # """
2399
+ # img = Image.open(image_path)
2400
+ # # Convert RGBA to RGB if needed (JPEG doesn't support alpha)
2401
+ # if img.mode == "RGBA":
2402
+ # rgb_img = Image.new("RGB", img.size, (255, 255, 255))
2403
+ # rgb_img.paste(img, mask=img.split()[3]) # Use alpha channel as mask
2404
+ # img = rgb_img
2405
+ # elif img.mode != "RGB":
2406
+ # img = img.convert("RGB")
2407
+
2408
+ # # Save as JPEG with quality setting for compression
2409
+ # img.save(output_path, "JPEG", quality=quality, optimize=True)
2410
+
2411
+
2412
+ # def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
2413
+ # """
2414
+ # Convert mask image to RGBA format (black/white mask).
2415
+ # Standard convention: white (255) = area to remove, black (0) = area to keep
2416
+ # Returns RGBA with white in RGB channels where removal is needed, alpha=255
2417
+ # """
2418
+ # if img.mode != "RGBA":
2419
+ # # For RGB/Grayscale masks: white (value>128) = remove, black (value<=128) = keep
2420
+ # gray = img.convert("L")
2421
+ # arr = np.array(gray)
2422
+ # # Create proper black/white mask: white pixels (>128) = remove, black (<=128) = keep
2423
+ # mask_bw = np.where(arr > 128, 255, 0).astype(np.uint8)
2424
+
2425
+ # rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
2426
+ # rgba[:, :, 0] = mask_bw # R
2427
+ # rgba[:, :, 1] = mask_bw # G
2428
+ # rgba[:, :, 2] = mask_bw # B
2429
+ # rgba[:, :, 3] = 255 # Fully opaque
2430
+ # log.info(f"Loaded {img.mode} mask: {int((mask_bw > 0).sum())} white pixels (to remove)")
2431
+ # return rgba
2432
+
2433
+ # # For RGBA: check if alpha channel is meaningful
2434
+ # arr = np.array(img)
2435
+ # alpha = arr[:, :, 3]
2436
+ # rgb = arr[:, :, :3]
2437
+
2438
+ # # If alpha is mostly opaque everywhere (mean > 200), treat RGB channels as mask values
2439
+ # if alpha.mean() > 200:
2440
+ # # Use RGB to determine mask: white/bright in RGB = remove
2441
+ # gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
2442
+ # # Also detect magenta specifically
2443
+ # magenta = np.all(rgb == [255, 0, 255], axis=2).astype(np.uint8) * 255
2444
+ # mask_bw = np.maximum(np.where(gray > 128, 255, 0).astype(np.uint8), magenta)
2445
+
2446
+ # rgba = arr.copy()
2447
+ # rgba[:, :, 0] = mask_bw # R
2448
+ # rgba[:, :, 1] = mask_bw # G
2449
+ # rgba[:, :, 2] = mask_bw # B
2450
+ # rgba[:, :, 3] = 255 # Fully opaque
2451
+ # log.info(f"Loaded RGBA mask (RGB-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2452
+ # return rgba
2453
+
2454
+ # # Alpha channel encodes the mask - convert to RGB-based
2455
+ # # Transparent areas (alpha < 128) = remove, Opaque areas = keep
2456
+ # mask_bw = np.where(alpha < 128, 255, 0).astype(np.uint8)
2457
+ # rgba = arr.copy()
2458
+ # rgba[:, :, 0] = mask_bw
2459
+ # rgba[:, :, 1] = mask_bw
2460
+ # rgba[:, :, 2] = mask_bw
2461
+ # rgba[:, :, 3] = 255
2462
+ # log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2463
+ # return rgba
2464
+
2465
+ # @app.post("/inpaint")
2466
+ # def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2467
+ # start_time = time.time()
2468
+ # status = "success"
2469
+ # error_msg = None
2470
+ # output_name = None
2471
+ # compressed_url = None
2472
+
2473
+ # try:
2474
+ # # Handle appname="collage-maker": get category_id from collage-maker if not provided
2475
+ # category_id = req.category_id
2476
+ # if req.appname == "collage-maker" and not category_id:
2477
+ # category_id = get_category_id_from_collage_maker()
2478
+ # if category_id:
2479
+ # log.info("Using category_id from collage-maker: %s", category_id)
2480
+
2481
+ # img_rgba = _load_rgba_image_from_gridfs(req.image_id, "image")
2482
+ # mask_img = _load_rgba_image_from_gridfs(req.mask_id, "mask")
2483
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2484
+
2485
+ # if req.passthrough:
2486
+ # result = np.array(img_rgba.convert("RGB"))
2487
+ # else:
2488
+ # result = process_inpaint(
2489
+ # np.array(img_rgba),
2490
+ # mask_rgba,
2491
+ # invert_mask=req.invert_mask,
2492
+ # prompt=req.prompt,
2493
+ # )
2494
+
2495
+ # output_name = f"output_{uuid.uuid4().hex}.png"
2496
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
2497
+
2498
+ # Image.fromarray(result).save(
2499
+ # output_path, "PNG", optimize=False, compress_level=1
2500
+ # )
2501
+
2502
+ # # Create compressed version
2503
+ # compressed_name = f"compressed_{output_name.replace('.png', '.jpg')}"
2504
+ # compressed_path = os.path.join(OUTPUT_DIR, compressed_name)
2505
+ # try:
2506
+ # # _compress_image(output_path, compressed_path, quality=85)
2507
+ # # # compressed_url = str(request.url_for("download_file", filename=compressed_name))
2508
+ # # compressed_url = str(request.url_for("download_file", filename=compressed_name).replace("http://", "https://"))
2509
+ # _compress_image(output_path, compressed_path, quality=85)
2510
+ # compressed_url = str(
2511
+ # request.url_for("download_file", filename=compressed_name)
2512
+ # ).replace("http://", "https://")
2513
+ # except Exception as compress_err:
2514
+ # log.warning("Failed to create compressed image: %s", compress_err)
2515
+ # compressed_url = None
2516
+
2517
+ # log_media_click(req.user_id, category_id, req.appname)
2518
+ # response = {"result": output_name}
2519
+ # if compressed_url:
2520
+ # response["Compressed_Image_URL"] = compressed_url
2521
+ # return response
2522
+
2523
+ # except Exception as e:
2524
+ # status = "fail"
2525
+ # error_msg = str(e)
2526
+ # raise
2527
+
2528
+ # finally:
2529
+ # end_time = time.time()
2530
+ # response_time_ms = (end_time - start_time) * 1000
2531
+
2532
+ # # log_doc = {
2533
+ # # "input_image_id": req.image_id,
2534
+ # # "input_mask_id": req.mask_id,
2535
+ # # "output_id": output_name,
2536
+ # # "status": status,
2537
+ # # "timestamp": datetime.utcnow(),
2538
+ # # "ts": int(time.time()),
2539
+ # # "response_time_ms": response_time_ms
2540
+ # # }
2541
+ # log_doc = {
2542
+ # "endpoint": "/inpaint",
2543
+ # "status": status,
2544
+ # "response_time_ms": float(response_time_ms),
2545
+ # "timestamp": datetime.utcnow(),
2546
+ # "appname": req.appname if req.appname else "None",
2547
+ # "error": error_msg
2548
+ # }
2549
+
2550
+ # # Store appname in api_logs if provided
2551
+ # if req.appname:
2552
+ # log_doc["appname"] = req.appname
2553
+
2554
+ # if error_msg:
2555
+ # log_doc["error"] = error_msg
2556
+
2557
+ # # if mongo_logs is not None:
2558
+ # # try:
2559
+ # # log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
2560
+ # # log.debug("Log document: %s", log_doc)
2561
+ # # result = mongo_logs.insert_one(log_doc)
2562
+ # # log.info("Mongo log inserted successfully: _id=%s, output_id=%s, status=%s, db=%s, collection=%s",
2563
+ # # result.inserted_id, output_name, status, mongo_logs.database.name, mongo_logs.name)
2564
+
2565
+ # # # Verify the insert by reading it back
2566
+ # # try:
2567
+ # # verify_doc = mongo_logs.find_one({"_id": result.inserted_id})
2568
+ # # if verify_doc:
2569
+ # # log.info("Verified: Document exists in MongoDB after insert")
2570
+ # # else:
2571
+ # # log.error("WARNING: Document not found after insert! _id=%s", result.inserted_id)
2572
+ # # except Exception as verify_err:
2573
+ # # log.warning("Could not verify insert: %s", verify_err)
2574
+ # # except Exception as mongo_err:
2575
+ # # log.error("Mongo log insert failed: %s, log_doc=%s", mongo_err, log_doc, exc_info=True)
2576
+ # # else:
2577
+ # # log.warning("MongoDB not configured, skipping log insert")
2578
+ # if api_logs_collection is not None:
2579
+ # try:
2580
+ # api_logs_collection.insert_one(log_doc)
2581
+ # log.info("API log inserted into logs/objectRemover")
2582
+ # except Exception as e:
2583
+ # log.error("Failed to insert API log: %s", e)
2584
+
2585
+ # # @app.post("/inpaint")
2586
+ # # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2587
+ # # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2588
+ # # raise HTTPException(status_code=404, detail="image_id not found")
2589
+ # # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2590
+ # # raise HTTPException(status_code=404, detail="mask_id not found")
2591
+
2592
+ # # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2593
+ # # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
2594
+ # # mask_rgba = _load_rgba_mask_from_image(mask_img)
2595
+
2596
+ # # # Debug: check mask before processing
2597
+ # # white_pixels = int((mask_rgba[:,:,0] > 128).sum())
2598
+ # # log.info(f"Inpaint request: mask has {white_pixels} white pixels, invert_mask={req.invert_mask}")
2599
+
2600
+ # # if req.passthrough:
2601
+ # # result = np.array(img_rgba.convert("RGB"))
2602
+ # # else:
2603
+ # # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
2604
+ # # result_name = f"output_{uuid.uuid4().hex}.png"
2605
+ # # result_path = os.path.join(OUTPUT_DIR, result_name)
2606
+ # # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2607
+
2608
+ # # logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
2609
+ # # return {"result": result_name}
2610
+
2611
+
2612
+ # @app.post("/inpaint-url")
2613
+ # def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2614
+ # """Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
2615
+ # start_time = time.time()
2616
+ # status = "success"
2617
+ # error_msg = None
2618
+ # result_name = None
2619
+
2620
+ # try:
2621
+ # # Handle appname="collage-maker": get category_id from collage-maker if not provided
2622
+ # category_id = req.category_id
2623
+ # if req.appname == "collage-maker" and not category_id:
2624
+ # category_id = get_category_id_from_collage_maker()
2625
+ # if category_id:
2626
+ # log.info("Using category_id from collage-maker: %s", category_id)
2627
+
2628
+ # img_rgba = _load_rgba_image_from_gridfs(req.image_id, "image")
2629
+ # mask_img = _load_rgba_image_from_gridfs(req.mask_id, "mask") # may be RGB/gray/RGBA
2630
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2631
+
2632
+ # if req.passthrough:
2633
+ # result = np.array(img_rgba.convert("RGB"))
2634
+ # else:
2635
+ # result = process_inpaint(
2636
+ # np.array(img_rgba),
2637
+ # mask_rgba,
2638
+ # invert_mask=req.invert_mask,
2639
+ # prompt=req.prompt,
2640
+ # )
2641
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2642
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2643
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2644
+
2645
+ # url = str(request.url_for("download_file", filename=result_name))
2646
+ # logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
2647
+ # log_media_click(req.user_id, category_id, req.appname)
2648
+ # return {"result": result_name, "url": url}
2649
+ # except Exception as e:
2650
+ # status = "fail"
2651
+ # error_msg = str(e)
2652
+ # raise
2653
+ # finally:
2654
+ # # Always log to regular MongoDB (mandatory)
2655
+ # end_time = time.time()
2656
+ # response_time_ms = (end_time - start_time) * 1000
2657
+ # log_doc = {
2658
+ # "input_image_id": req.image_id,
2659
+ # "input_mask_id": req.mask_id,
2660
+ # "output_id": result_name,
2661
+ # "status": status,
2662
+ # "timestamp": datetime.utcnow(),
2663
+ # "ts": int(time.time()),
2664
+ # "response_time_ms": response_time_ms,
2665
+ # }
2666
+ # # Store appname in api_logs if provided
2667
+ # if req.appname:
2668
+ # log_doc["appname"] = req.appname
2669
+ # if error_msg:
2670
+ # log_doc["error"] = error_msg
2671
+ # if mongo_logs is not None:
2672
+ # try:
2673
+ # log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
2674
+ # result = mongo_logs.insert_one(log_doc)
2675
+ # log.info("Mongo log inserted successfully: _id=%s, output_id=%s, status=%s, db=%s, collection=%s",
2676
+ # result.inserted_id, output_name, status, mongo_logs.database.name, mongo_logs.name)
2677
+
2678
+ # # Verify the insert by reading it back
2679
+ # try:
2680
+ # verify_doc = mongo_logs.find_one({"_id": result.inserted_id})
2681
+ # if verify_doc:
2682
+ # log.info("Verified: Document exists in MongoDB after insert")
2683
+ # else:
2684
+ # log.error("WARNING: Document not found after insert! _id=%s", result.inserted_id)
2685
+ # except Exception as verify_err:
2686
+ # log.warning("Could not verify insert: %s", verify_err)
2687
+ # except Exception as mongo_err:
2688
+ # log.error("Mongo log insert failed: %s, log_doc=%s", mongo_err, log_doc, exc_info=True)
2689
+ # else:
2690
+ # log.warning("MongoDB not configured, skipping log insert")
2691
+
2692
+
2693
+ # @app.post("/inpaint-multipart")
2694
+ # def inpaint_multipart(
2695
+ # image: UploadFile = File(...),
2696
+ # mask: UploadFile = File(...),
2697
+ # request: Request = None,
2698
+ # invert_mask: bool = True,
2699
+ # mask_is_painted: bool = False, # if True, mask file is the painted-on image (e.g., black strokes on original)
2700
+ # passthrough: bool = False,
2701
+ # prompt: Optional[str] = Form(None),
2702
+ # user_id: Optional[str] = Form(None),
2703
+ # category_id: Optional[str] = Form(None),
2704
+ # appname: Optional[str] = Form(None),
2705
+ # _: None = Depends(bearer_auth),
2706
+ # ) -> Dict[str, str]:
2707
+ # start_time = time.time()
2708
+ # status = "success"
2709
+ # error_msg = None
2710
+ # result_name = None
2711
+
2712
+ # try:
2713
+ # # Handle appname="collage-maker": get category_id from collage-maker if not provided
2714
+ # final_category_id = category_id
2715
+ # if appname == "collage-maker" and not final_category_id:
2716
+ # final_category_id = get_category_id_from_collage_maker()
2717
+ # if final_category_id:
2718
+ # log.info("Using category_id from collage-maker: %s", final_category_id)
2719
+
2720
+ # # Load in-memory
2721
+ # img = Image.open(image.file).convert("RGBA")
2722
+ # m = Image.open(mask.file).convert("RGBA")
2723
+
2724
+ # if passthrough:
2725
+ # # Just echo the input image, ignore mask
2726
+ # result = np.array(img.convert("RGB"))
2727
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2728
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2729
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2730
+
2731
+ # url: Optional[str] = None
2732
+ # try:
2733
+ # if request is not None:
2734
+ # url = str(request.url_for("download_file", filename=result_name))
2735
+ # except Exception:
2736
+ # url = None
2737
+
2738
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2739
+ # if url:
2740
+ # entry["url"] = url
2741
+ # logs.append(entry)
2742
+ # resp: Dict[str, str] = {"result": result_name}
2743
+ # if url:
2744
+ # resp["url"] = url
2745
+ # log_media_click(user_id, final_category_id, appname)
2746
+ # return resp
2747
+
2748
+ # if mask_is_painted:
2749
+ # # Auto-detect pink/magenta paint and convert to black/white mask
2750
+ # # White pixels = areas to remove, Black pixels = areas to keep
2751
+ # log.info("Auto-detecting pink/magenta paint from uploaded image...")
2752
+
2753
+ # m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
2754
+
2755
+ # # Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
2756
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2757
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2758
+ # magenta_detected = (
2759
+ # (m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
2760
+ # (m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
2761
+ # (m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
2762
+ # ).astype(np.uint8) * 255
2763
+
2764
+ # # Method 2: Also check if original image was provided to find differences
2765
+ # if img is not None:
2766
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2767
+ # if img_rgb.shape == m_rgb.shape:
2768
+ # diff = cv2.absdiff(img_rgb, m_rgb)
2769
+ # gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
2770
+ # # Any significant difference (>50) could be paint
2771
+ # diff_mask = (gray_diff > 50).astype(np.uint8) * 255
2772
+ # # Combine with magenta detection
2773
+ # binmask = cv2.bitwise_or(magenta_detected, diff_mask)
2774
+ # else:
2775
+ # binmask = magenta_detected
2776
+ # else:
2777
+ # # No original image provided, use magenta detection only
2778
+ # binmask = magenta_detected
2779
+
2780
+ # # Clean up the mask: remove noise and fill small holes
2781
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2782
+ # # Close small gaps in the mask
2783
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2784
+ # # Remove small noise
2785
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2786
+
2787
+ # nonzero = int((binmask > 0).sum())
2788
+ # log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
2789
+
2790
+ # # If very few pixels detected, assume the user may already be providing a BW mask
2791
+ # # and proceed without forcing strict detection
2792
+
2793
+ # if nonzero < 50:
2794
+ # log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
2795
+ # result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
2796
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2797
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2798
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2799
+ # return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
2800
+
2801
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
2802
+ # # Encode in RGBA format for process_inpaint
2803
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
2804
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
2805
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
2806
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
2807
+ # mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
2808
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
2809
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
2810
+ # # Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
2811
+ # mask_rgba[:, :, 3] = 255 - binmask
2812
+
2813
+ # log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
2814
+ # nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
2815
+ # else:
2816
+ # mask_rgba = _load_rgba_mask_from_image(m)
2817
+
2818
+ # # When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
2819
+ # actual_invert = invert_mask # Use default True for painted masks
2820
+ # log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
2821
+
2822
+ # result = process_inpaint(
2823
+ # np.array(img),
2824
+ # mask_rgba,
2825
+ # invert_mask=actual_invert,
2826
+ # prompt=prompt,
2827
+ # )
2828
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2829
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2830
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2831
+
2832
+ # url: Optional[str] = None
2833
+ # try:
2834
+ # if request is not None:
2835
+ # url = str(request.url_for("download_file", filename=result_name))
2836
+ # except Exception:
2837
+ # url = None
2838
+
2839
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2840
+ # if url:
2841
+ # entry["url"] = url
2842
+ # logs.append(entry)
2843
+ # resp: Dict[str, str] = {"result": result_name}
2844
+ # if url:
2845
+ # resp["url"] = url
2846
+ # log_media_click(user_id, final_category_id, appname)
2847
+ # return resp
2848
+ # except Exception as e:
2849
+ # status = "fail"
2850
+ # error_msg = str(e)
2851
+ # raise
2852
+ # finally:
2853
+ # # Always log to regular MongoDB (mandatory)
2854
+ # end_time = time.time()
2855
+ # response_time_ms = (end_time - start_time) * 1000
2856
+ # log_doc = {
2857
+ # "endpoint": "inpaint-multipart",
2858
+ # "output_id": result_name,
2859
+ # "status": status,
2860
+ # "timestamp": datetime.utcnow(),
2861
+ # "ts": int(time.time()),
2862
+ # "response_time_ms": response_time_ms,
2863
+ # }
2864
+ # # Store appname in api_logs if provided
2865
+ # if appname:
2866
+ # log_doc["appname"] = appname
2867
+ # if error_msg:
2868
+ # log_doc["error"] = error_msg
2869
+ # if mongo_logs is not None:
2870
+ # try:
2871
+ # log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
2872
+ # result = mongo_logs.insert_one(log_doc)
2873
+ # log.info("Mongo log inserted successfully: _id=%s, output_id=%s, status=%s, db=%s, collection=%s",
2874
+ # result.inserted_id, output_name, status, mongo_logs.database.name, mongo_logs.name)
2875
+
2876
+ # # Verify the insert by reading it back
2877
+ # try:
2878
+ # verify_doc = mongo_logs.find_one({"_id": result.inserted_id})
2879
+ # if verify_doc:
2880
+ # log.info("Verified: Document exists in MongoDB after insert")
2881
+ # else:
2882
+ # log.error("WARNING: Document not found after insert! _id=%s", result.inserted_id)
2883
+ # except Exception as verify_err:
2884
+ # log.warning("Could not verify insert: %s", verify_err)
2885
+ # except Exception as mongo_err:
2886
+ # log.error("Mongo log insert failed: %s, log_doc=%s", mongo_err, log_doc, exc_info=True)
2887
+ # else:
2888
+ # log.warning("MongoDB not configured, skipping log insert")
2889
+
2890
+
2891
+ # @app.post("/remove-pink")
2892
+ # def remove_pink_segments(
2893
+ # image: UploadFile = File(...),
2894
+ # request: Request = None,
2895
+ # user_id: Optional[str] = Form(None),
2896
+ # category_id: Optional[str] = Form(None),
2897
+ # appname: Optional[str] = Form(None),
2898
+ # _: None = Depends(bearer_auth),
2899
+ # ) -> Dict[str, str]:
2900
+ # """
2901
+ # Simple endpoint: upload an image with pink/magenta segments to remove.
2902
+ # - Pink/Magenta segments → automatically removed (white in mask)
2903
+ # - Everything else → automatically kept (black in mask)
2904
+ # Just paint pink/magenta on areas you want to remove, upload the image, and it works!
2905
+ # """
2906
+ # start_time = time.time()
2907
+ # status = "success"
2908
+ # error_msg = None
2909
+ # result_name = None
2910
+
2911
+ # try:
2912
+ # # Handle appname="collage-maker": get category_id from collage-maker if not provided
2913
+ # final_category_id = category_id
2914
+ # if appname == "collage-maker" and not final_category_id:
2915
+ # final_category_id = get_category_id_from_collage_maker()
2916
+ # if final_category_id:
2917
+ # log.info("Using category_id from collage-maker: %s", final_category_id)
2918
+
2919
+ # log.info(f"Simple remove-pink: processing image {image.filename}")
2920
+
2921
+ # # Load the image (with pink paint on it)
2922
+ # img = Image.open(image.file).convert("RGBA")
2923
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2924
+
2925
+ # # Auto-detect pink/magenta segments to remove
2926
+ # # Pink/Magenta → white in mask (remove)
2927
+ # # Everything else (natural image colors, including dark areas) → black in mask (keep)
2928
+
2929
+ # # Detect pink/magenta using fixed RGB bounds per requested logic
2930
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2931
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2932
+ # binmask = (
2933
+ # (img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
2934
+ # (img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
2935
+ # (img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
2936
+ # ).astype(np.uint8) * 255
2937
+
2938
+ # # Clean up the pink mask
2939
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2940
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2941
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2942
+
2943
+ # nonzero = int((binmask > 0).sum())
2944
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
2945
+ # log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
2946
+
2947
+ # # Debug: log bounds used
2948
+ # log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
2949
+
2950
+ # if nonzero < 50:
2951
+ # log.error("No pink segments detected! Returning original image.")
2952
+ # result = np.array(img.convert("RGB"))
2953
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2954
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2955
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2956
+ # return {
2957
+ # "result": result_name,
2958
+ # "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
2959
+ # }
2960
+
2961
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
2962
+ # # Encode in RGBA format that process_inpaint expects
2963
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
2964
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
2965
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
2966
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
2967
+ # # RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
2968
+ # mask_rgba[:, :, 0] = binmask # R: white where pink
2969
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
2970
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
2971
+ # # Alpha: 0 (transparent) where pink → will become white after 255-alpha
2972
+ # # 255 (opaque) everywhere else → will become black after 255-alpha
2973
+ # mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
2974
+
2975
+ # # Verify mask encoding
2976
+ # alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
2977
+ # alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
2978
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
2979
+ # log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
2980
+ # log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
2981
+
2982
+ # # IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
2983
+ # # Remove pink from the original image before processing
2984
+ # # Create a clean version: where pink was detected, keep original image colors
2985
+ # img_clean = np.array(img.convert("RGBA"))
2986
+ # # Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
2987
+ # # Actually, the model will inpaint over those areas, so we can pass the original
2988
+ # # But for better results, we might want to remove the pink overlay first
2989
+
2990
+ # # Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
2991
+ # log.info(f"Starting inpainting process...")
2992
+ # result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
2993
+ # log.info(f"Inpainting complete, result shape: {result.shape}")
2994
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2995
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2996
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2997
+
2998
+ # url: Optional[str] = None
2999
+ # try:
3000
+ # if request is not None:
3001
+ # url = str(request.url_for("download_file", filename=result_name))
3002
+ # except Exception:
3003
+ # url = None
3004
+
3005
+ # logs.append({
3006
+ # "result": result_name,
3007
+ # "filename": image.filename,
3008
+ # "pink_pixels": nonzero,
3009
+ # "timestamp": datetime.utcnow().isoformat()
3010
+ # })
3011
+
3012
+ # resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
3013
+ # if url:
3014
+ # resp["url"] = url
3015
+ # log_media_click(user_id, final_category_id, appname)
3016
+ # return resp
3017
+ # except Exception as e:
3018
+ # status = "fail"
3019
+ # error_msg = str(e)
3020
+ # raise
3021
+ # finally:
3022
+ # # Always log to regular MongoDB (mandatory)
3023
+ # end_time = time.time()
3024
+ # response_time_ms = (end_time - start_time) * 1000
3025
+ # log_doc = {
3026
+ # "endpoint": "remove-pink",
3027
+ # "output_id": result_name,
3028
+ # "status": status,
3029
+ # "timestamp": datetime.utcnow(),
3030
+ # "ts": int(time.time()),
3031
+ # "response_time_ms": response_time_ms,
3032
+ # }
3033
+ # # Store appname in api_logs if provided
3034
+ # if appname:
3035
+ # log_doc["appname"] = appname
3036
+ # if error_msg:
3037
+ # log_doc["error"] = error_msg
3038
+ # if mongo_logs is not None:
3039
+ # try:
3040
+ # log.info("Inserting log to MongoDB - Database: %s, Collection: %s", mongo_logs.database.name, mongo_logs.name)
3041
+ # result = mongo_logs.insert_one(log_doc)
3042
+ # log.info("Mongo log inserted successfully: _id=%s, output_id=%s, status=%s, db=%s, collection=%s",
3043
+ # result.inserted_id, output_name, status, mongo_logs.database.name, mongo_logs.name)
3044
+
3045
+ # # Verify the insert by reading it back
3046
+ # try:
3047
+ # verify_doc = mongo_logs.find_one({"_id": result.inserted_id})
3048
+ # if verify_doc:
3049
+ # log.info("Verified: Document exists in MongoDB after insert")
3050
+ # else:
3051
+ # log.error("WARNING: Document not found after insert! _id=%s", result.inserted_id)
3052
+ # except Exception as verify_err:
3053
+ # log.warning("Could not verify insert: %s", verify_err)
3054
+ # except Exception as mongo_err:
3055
+ # log.error("Mongo log insert failed: %s, log_doc=%s", mongo_err, log_doc, exc_info=True)
3056
+ # else:
3057
+ # log.warning("MongoDB not configured, skipping log insert")
3058
+
3059
+
3060
+ # @app.get("/download/{filename}")
3061
+ # def download_file(filename: str):
3062
+ # path = os.path.join(OUTPUT_DIR, filename)
3063
+ # if not os.path.isfile(path):
3064
+ # raise HTTPException(status_code=404, detail="file not found")
3065
+ # return FileResponse(path)
3066
+
3067
+
3068
+ # @app.get("/result/{filename}")
3069
+ # def view_result(filename: str):
3070
+ # """View result image directly in browser (same as download but with proper content-type for viewing)"""
3071
+ # path = os.path.join(OUTPUT_DIR, filename)
3072
+ # if not os.path.isfile(path):
3073
+ # raise HTTPException(status_code=404, detail="file not found")
3074
+ # return FileResponse(path, media_type="image/png")
3075
+
3076
+
3077
+ # @app.get("/logs")
3078
+ # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
3079
+ # return JSONResponse(content=logs)