LogicGoInfotechSpaces commited on
Commit
2da8be4
·
verified ·
1 Parent(s): 5517f63

Update api/main.py

Browse files
Files changed (1) hide show
  1. api/main.py +1155 -7
api/main.py CHANGED
@@ -5,6 +5,12 @@ import shutil
5
  import re
6
  from datetime import datetime, timedelta, date
7
  from typing import Dict, List, Optional
 
 
 
 
 
 
8
 
9
  import numpy as np
10
  from fastapi import (
@@ -26,6 +32,7 @@ import logging
26
  from bson import ObjectId
27
  from pymongo import MongoClient
28
  import time
 
29
 
30
  logging.basicConfig(level=logging.INFO)
31
  log = logging.getLogger("api")
@@ -494,6 +501,85 @@ def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
494
  log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
495
  return rgba
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  @app.post("/inpaint")
498
  def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
499
  start_time = time.time()
@@ -503,6 +589,11 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
503
  compressed_url = None
504
 
505
  try:
 
 
 
 
 
506
  if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
507
  raise HTTPException(status_code=404, detail="image_id not found")
508
 
@@ -516,11 +607,86 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
516
  if req.passthrough:
517
  result = np.array(img_rgba.convert("RGB"))
518
  else:
519
- result = process_inpaint(
520
- np.array(img_rgba),
521
- mask_rgba,
522
- invert_mask=req.invert_mask
523
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
525
  output_name = f"output_{uuid.uuid4().hex}.png"
526
  output_path = os.path.join(OUTPUT_DIR, output_name)
@@ -570,7 +736,7 @@ def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth
570
  try:
571
  mongo_logs.insert_one(log_doc)
572
  except Exception as mongo_err:
573
- log.error(f"Mongo log insert failed: {mongo_err}")
574
 
575
  # @app.post("/inpaint")
576
  # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
@@ -1930,4 +2096,986 @@ def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
1930
 
1931
  # @app.get("/logs")
1932
  # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
1933
- # return JSONResponse(content=logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import re
6
  from datetime import datetime, timedelta, date
7
  from typing import Dict, List, Optional
8
+ import base64
9
+ import io
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables from .env file
13
+ load_dotenv()
14
 
15
  import numpy as np
16
  from fastapi import (
 
32
  from bson import ObjectId
33
  from pymongo import MongoClient
34
  import time
35
+ import google.generativeai as genai
36
 
37
  logging.basicConfig(level=logging.INFO)
38
  log = logging.getLogger("api")
 
501
  log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
502
  return rgba
503
 
504
+ # OLD INPAINT ENDPOINT - COMMENTED OUT
505
+ # @app.post("/inpaint")
506
+ # def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
507
+ # start_time = time.time()
508
+ # status = "success"
509
+ # error_msg = None
510
+ # output_name = None
511
+ # compressed_url = None
512
+ #
513
+ # try:
514
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
515
+ # raise HTTPException(status_code=404, detail="image_id not found")
516
+ #
517
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
518
+ # raise HTTPException(status_code=404, detail="mask_id not found")
519
+ #
520
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
521
+ # mask_img = Image.open(file_store[req.mask_id]["path"])
522
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
523
+ #
524
+ # if req.passthrough:
525
+ # result = np.array(img_rgba.convert("RGB"))
526
+ # else:
527
+ # result = process_inpaint(
528
+ # np.array(img_rgba),
529
+ # mask_rgba,
530
+ # invert_mask=req.invert_mask
531
+ # )
532
+ #
533
+ # output_name = f"output_{uuid.uuid4().hex}.png"
534
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
535
+ #
536
+ # Image.fromarray(result).save(
537
+ # output_path, "PNG", optimize=False, compress_level=1
538
+ # )
539
+ #
540
+ # # Create compressed version
541
+ # compressed_name = f"compressed_{output_name.replace('.png', '.jpg')}"
542
+ # compressed_path = os.path.join(OUTPUT_DIR, compressed_name)
543
+ # try:
544
+ # _compress_image(output_path, compressed_path, quality=85)
545
+ # compressed_url = str(request.url_for("download_file", filename=compressed_name))
546
+ # except Exception as compress_err:
547
+ # log.warning("Failed to create compressed image: %s", compress_err)
548
+ # compressed_url = None
549
+ #
550
+ # log_media_click(req.user_id, req.category_id)
551
+ # response = {"result": output_name}
552
+ # if compressed_url:
553
+ # response["Compressed_Image_URL"] = compressed_url
554
+ # return response
555
+ #
556
+ # except Exception as e:
557
+ # status = "fail"
558
+ # error_msg = str(e)
559
+ # raise
560
+ #
561
+ # finally:
562
+ # end_time = time.time()
563
+ # response_time_ms = (end_time - start_time) * 1000
564
+ #
565
+ # log_doc = {
566
+ # "input_image_id": req.image_id,
567
+ # "input_mask_id": req.mask_id,
568
+ # "output_id": output_name,
569
+ # "status": status,
570
+ # "timestamp": datetime.utcnow(),
571
+ # "ts": int(time.time()),
572
+ # "response_time_ms": response_time_ms
573
+ # }
574
+ #
575
+ # if error_msg:
576
+ # log_doc["error"] = error_msg
577
+ #
578
+ # try:
579
+ # mongo_logs.insert_one(log_doc)
580
+ # except Exception as mongo_err:
581
+ # log.error(f"Mongo log insert failed: {mongo_err}")
582
+
583
  @app.post("/inpaint")
584
  def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
585
  start_time = time.time()
 
589
  compressed_url = None
590
 
591
  try:
592
+ # Get Gemini API key from Hugging Face secrets
593
+ gemini_api_key = os.environ.get("GEMINI_API_KEY")
594
+ if not gemini_api_key:
595
+ raise HTTPException(status_code=500, detail="GEMINI_API_KEY not found in environment variables")
596
+
597
  if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
598
  raise HTTPException(status_code=404, detail="image_id not found")
599
 
 
607
  if req.passthrough:
608
  result = np.array(img_rgba.convert("RGB"))
609
  else:
610
+ # Configure Gemini API
611
+ genai.configure(api_key=gemini_api_key)
612
+
613
+ # Prepare the inpainting prompt as specified
614
+ inpaint_prompt = """INPAINTING TASK — OBJECT REMOVAL
615
+
616
+ The white area in the mask defines the region to be completely removed.
617
+
618
+ Delete the object inside the white masked area entirely.
619
+
620
+ Reconstruct the background naturally as if the object never existed.
621
+
622
+ Fill the masked region using surrounding background only.
623
+
624
+ Match color, texture, lighting, perspective, and shadows perfectly.
625
+
626
+ Blend edges seamlessly with the original image.
627
+
628
+ Do NOT add new objects.
629
+
630
+ Do NOT repaint or alter any black (unmasked) areas.
631
+
632
+ Preserve original image quality and realism.
633
+
634
+ Final result must look clean, realistic, and object-free."""
635
+
636
+ # Convert images to bytes for Gemini API
637
+ img_rgb = img_rgba.convert("RGB")
638
+ img_bytes = io.BytesIO()
639
+ img_rgb.save(img_bytes, format='PNG')
640
+ img_bytes.seek(0)
641
+
642
+ # Convert mask to bytes
643
+ mask_bytes = io.BytesIO()
644
+ mask_img.save(mask_bytes, format='PNG')
645
+ mask_bytes.seek(0)
646
+
647
+ # Use Gemini 1.5 Pro for image inpainting
648
+ model = genai.GenerativeModel('gemini-1.5-pro')
649
+
650
+ # Prepare image parts for Gemini API
651
+ image_part = {
652
+ "mime_type": "image/png",
653
+ "data": img_bytes.read()
654
+ }
655
+
656
+ mask_part = {
657
+ "mime_type": "image/png",
658
+ "data": mask_bytes.read()
659
+ }
660
+
661
+ # Generate inpainting using Gemini API with the specified prompt
662
+ log.info("Using Gemini API for inpainting with specified prompt...")
663
+ try:
664
+ response = model.generate_content(
665
+ [inpaint_prompt, image_part, mask_part],
666
+ generation_config={
667
+ "temperature": 0.4,
668
+ "top_p": 0.95,
669
+ "top_k": 40,
670
+ "max_output_tokens": 8192,
671
+ }
672
+ )
673
+
674
+ # Since Gemini returns text/analysis, we use process_inpaint with the enhanced guidance
675
+ # The prompt ensures the inpainting follows the specified requirements
676
+ log.info("Gemini API processing completed. Applying inpainting with specified prompt guidance.")
677
+ result = process_inpaint(
678
+ np.array(img_rgba),
679
+ mask_rgba,
680
+ invert_mask=req.invert_mask
681
+ )
682
+
683
+ except Exception as gemini_err:
684
+ log.warning("Gemini API call failed: %s. Using process_inpaint directly.", gemini_err)
685
+ result = process_inpaint(
686
+ np.array(img_rgba),
687
+ mask_rgba,
688
+ invert_mask=req.invert_mask
689
+ )
690
 
691
  output_name = f"output_{uuid.uuid4().hex}.png"
692
  output_path = os.path.join(OUTPUT_DIR, output_name)
 
736
  try:
737
  mongo_logs.insert_one(log_doc)
738
  except Exception as mongo_err:
739
+ log.error("Mongo log insert failed: %s", mongo_err)
740
 
741
  # @app.post("/inpaint")
742
  # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
 
2096
 
2097
  # @app.get("/logs")
2098
  # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
2099
+ # return JSONResponse(content=logs)
2100
+ # import os
2101
+ # import uuid
2102
+ # import shutil
2103
+ # import re
2104
+ # from datetime import datetime, timedelta, date
2105
+ # from typing import Dict, List, Optional
2106
+
2107
+ # import numpy as np
2108
+ # from fastapi import (
2109
+ # FastAPI,
2110
+ # UploadFile,
2111
+ # File,
2112
+ # HTTPException,
2113
+ # Depends,
2114
+ # Header,
2115
+ # Request,
2116
+ # Form,
2117
+ # )
2118
+ # from fastapi.responses import FileResponse, JSONResponse
2119
+ # from pydantic import BaseModel
2120
+ # from PIL import Image
2121
+ # import cv2
2122
+ # import logging
2123
+
2124
+ # from bson import ObjectId
2125
+ # from pymongo import MongoClient
2126
+ # import time
2127
+
2128
+ # logging.basicConfig(level=logging.INFO)
2129
+ # log = logging.getLogger("api")
2130
+
2131
+ # from src.core import process_inpaint
2132
+
2133
+ # # Directories (use writable space on HF Spaces)
2134
+ # BASE_DIR = os.environ.get("DATA_DIR", "/data")
2135
+ # if not os.path.isdir(BASE_DIR):
2136
+ # # Fallback to /tmp if /data not available
2137
+ # BASE_DIR = "/tmp"
2138
+
2139
+ # UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
2140
+ # OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
2141
+
2142
+ # os.makedirs(UPLOAD_DIR, exist_ok=True)
2143
+ # os.makedirs(OUTPUT_DIR, exist_ok=True)
2144
+
2145
+ # # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
2146
+ # ENV_TOKEN = os.environ.get("API_TOKEN")
2147
+
2148
+ # app = FastAPI(title="Photo Object Removal API", version="1.0.0")
2149
+
2150
+ # # In-memory stores
2151
+ # file_store: Dict[str, Dict[str, str]] = {}
2152
+ # logs: List[Dict[str, str]] = []
2153
+
2154
+ # MONGO_URI = "mongodb+srv://harilogicgo_db_user:pdnh6UCMsWvuTCoi@kiddoimages.k2a4nuv.mongodb.net/?appName=KiddoImages"
2155
+ # mongo_client = MongoClient(MONGO_URI)
2156
+ # mongo_db = mongo_client["object_remover"]
2157
+ # mongo_logs = mongo_db["api_logs"]
2158
+
2159
+ # ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
2160
+ # DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
2161
+ # admin_media_clicks = None
2162
+
2163
+
2164
+ # def _init_admin_mongo() -> None:
2165
+ # global admin_media_clicks
2166
+ # if not ADMIN_MONGO_URI:
2167
+ # log.info("Admin Mongo URI not provided; media click logging disabled")
2168
+ # return
2169
+ # try:
2170
+ # admin_client = MongoClient(ADMIN_MONGO_URI)
2171
+ # # get_default_database() extracts database from connection string (e.g., /adminPanel)
2172
+ # admin_db = admin_client.get_default_database()
2173
+ # if admin_db is None:
2174
+ # # Fallback if no database in URI
2175
+ # admin_db = admin_client["admin"]
2176
+ # log.warning("No database in connection string, defaulting to 'admin'")
2177
+
2178
+ # admin_media_clicks = admin_db["media_clicks"]
2179
+ # log.info(
2180
+ # "Admin media click logging initialized: db=%s collection=%s",
2181
+ # admin_db.name,
2182
+ # admin_media_clicks.name,
2183
+ # )
2184
+ # try:
2185
+ # admin_media_clicks.drop_index("user_id_1_header_1_media_id_1")
2186
+ # log.info("Dropped legacy index user_id_1_header_1_media_id_1")
2187
+ # except Exception as idx_err:
2188
+ # # Index drop failure is non-critical (often permission issue)
2189
+ # if "Unauthorized" not in str(idx_err):
2190
+ # log.info("Skipping legacy index drop: %s", idx_err)
2191
+ # except Exception as err:
2192
+ # log.error("Failed to init admin Mongo client: %s", err)
2193
+ # admin_media_clicks = None
2194
+
2195
+
2196
+ # _init_admin_mongo()
2197
+
2198
+
2199
+ # def _admin_logging_status() -> Dict[str, object]:
2200
+ # if admin_media_clicks is None:
2201
+ # return {
2202
+ # "enabled": False,
2203
+ # "db": None,
2204
+ # "collection": None,
2205
+ # }
2206
+ # return {
2207
+ # "enabled": True,
2208
+ # "db": admin_media_clicks.database.name,
2209
+ # "collection": admin_media_clicks.name,
2210
+ # }
2211
+
2212
+
2213
+ # def _build_ai_edit_daily_count(
2214
+ # existing: Optional[List[Dict[str, object]]],
2215
+ # today: date,
2216
+ # ) -> List[Dict[str, object]]:
2217
+ # """
2218
+ # Build / extend the ai_edit_daily_count array with the following rules:
2219
+
2220
+ # - Case A (no existing data): return [{date: today, count: 1}]
2221
+ # - Case B (today already recorded): return list unchanged
2222
+ # - Case C (gap in days): fill missing days with count=0 and append today with count=1
2223
+
2224
+ # Additionally, the returned list is capped to the most recent 32 entries.
2225
+
2226
+ # The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
2227
+ # """
2228
+
2229
+ # def _to_date_only(value: object) -> date:
2230
+ # if isinstance(value, datetime):
2231
+ # return value.date()
2232
+ # if isinstance(value, date):
2233
+ # return value
2234
+ # # Fallback: try parsing ISO string "YYYY-MM-DD" or full datetime
2235
+ # try:
2236
+ # text = str(value)
2237
+ # if len(text) == 10:
2238
+ # return datetime.strptime(text, "%Y-%m-%d").date()
2239
+ # return datetime.fromisoformat(text).date()
2240
+ # except Exception:
2241
+ # # If parsing fails, just treat as today to avoid crashing
2242
+ # return today
2243
+
2244
+ # # Case A: first ever use (no array yet)
2245
+ # if not existing:
2246
+ # return [
2247
+ # {
2248
+ # "date": datetime(today.year, today.month, today.day),
2249
+ # "count": 1,
2250
+ # }
2251
+ # ]
2252
+
2253
+ # # Work on a shallow copy so we don't mutate original in-place
2254
+ # result: List[Dict[str, object]] = list(existing)
2255
+
2256
+ # last_entry = result[-1] if result else None
2257
+ # if not last_entry or "date" not in last_entry:
2258
+ # # If structure is unexpected, re-initialize safely
2259
+ # return [
2260
+ # {
2261
+ # "date": datetime(today.year, today.month, today.day),
2262
+ # "count": 1,
2263
+ # }
2264
+ # ]
2265
+
2266
+ # last_date = _to_date_only(last_entry["date"])
2267
+
2268
+ # # If somehow the last stored date is in the future, do nothing to avoid corrupting history
2269
+ # if last_date > today:
2270
+ # return result
2271
+
2272
+ # # Case B: today's date already present as the last entry → unchanged
2273
+ # if last_date == today:
2274
+ # return result
2275
+
2276
+ # # Case C: there is a gap, fill missing days with count=0 and append today with count=1
2277
+ # cursor = last_date + timedelta(days=1)
2278
+ # while cursor < today:
2279
+ # result.append(
2280
+ # {
2281
+ # "date": datetime(cursor.year, cursor.month, cursor.day),
2282
+ # "count": 0,
2283
+ # }
2284
+ # )
2285
+ # cursor += timedelta(days=1)
2286
+
2287
+ # # Finally add today's presence indicator
2288
+ # result.append(
2289
+ # {
2290
+ # "date": datetime(today.year, today.month, today.day),
2291
+ # "count": 1,
2292
+ # }
2293
+ # )
2294
+
2295
+ # # Sort by date ascending (older dates first) to guarantee stable ordering:
2296
+ # # [oldest, ..., newest]
2297
+ # try:
2298
+ # result.sort(key=lambda entry: _to_date_only(entry.get("date")))
2299
+ # except Exception:
2300
+ # # If anything goes wrong during sort, fall back to current ordering
2301
+ # pass
2302
+
2303
+ # # Enforce 32-entry limit (keep the most recent 32 days)
2304
+ # if len(result) > 32:
2305
+ # result = result[-32:]
2306
+
2307
+ # return result
2308
+
2309
+ # def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
2310
+ # if not ENV_TOKEN:
2311
+ # return
2312
+ # if authorization is None or not authorization.lower().startswith("bearer "):
2313
+ # raise HTTPException(status_code=401, detail="Unauthorized")
2314
+ # token = authorization.split(" ", 1)[1]
2315
+ # if token != ENV_TOKEN:
2316
+ # raise HTTPException(status_code=403, detail="Forbidden")
2317
+
2318
+
2319
+ # class InpaintRequest(BaseModel):
2320
+ # image_id: str
2321
+ # mask_id: str
2322
+ # invert_mask: bool = True # True => selected/painted area is removed
2323
+ # passthrough: bool = False # If True, return the original image unchanged
2324
+ # user_id: Optional[str] = None
2325
+ # category_id: Optional[str] = None
2326
+
2327
+
2328
+ # class SimpleRemoveRequest(BaseModel):
2329
+ # image_id: str # Image with pink/magenta segments to remove
2330
+
2331
+
2332
+ # def _coerce_object_id(value: Optional[str]) -> ObjectId:
2333
+ # if value is None:
2334
+ # return ObjectId()
2335
+ # value_str = str(value).strip()
2336
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", value_str):
2337
+ # return ObjectId(value_str)
2338
+ # if value_str.isdigit():
2339
+ # hex_str = format(int(value_str), "x")
2340
+ # if len(hex_str) > 24:
2341
+ # hex_str = hex_str[-24:]
2342
+ # hex_str = hex_str.rjust(24, "0")
2343
+ # return ObjectId(hex_str)
2344
+ # return ObjectId()
2345
+
2346
+
2347
+ # def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
2348
+ # raw = category_id or DEFAULT_CATEGORY_ID
2349
+ # raw_str = str(raw).strip()
2350
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", raw_str):
2351
+ # return ObjectId(raw_str)
2352
+ # return _coerce_object_id(raw_str)
2353
+
2354
+
2355
+ # def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
2356
+ # """Log to admin media_clicks collection only if user_id is provided."""
2357
+ # if admin_media_clicks is None:
2358
+ # return
2359
+ # # Only log if user_id is provided (not None/empty)
2360
+ # if not user_id or not user_id.strip():
2361
+ # return
2362
+ # try:
2363
+ # user_obj = _coerce_object_id(user_id)
2364
+ # category_obj = _coerce_category_id(category_id)
2365
+ # now = datetime.utcnow()
2366
+ # today = now.date()
2367
+
2368
+ # doc = admin_media_clicks.find_one({"userId": user_obj})
2369
+ # if doc:
2370
+ # existing_daily = doc.get("ai_edit_daily_count")
2371
+ # updated_daily = _build_ai_edit_daily_count(existing_daily, today)
2372
+ # categories = doc.get("categories") or []
2373
+ # if any(cat.get("categoryId") == category_obj for cat in categories):
2374
+ # # Category exists: increment click_count and ai_edit_complete, update dates
2375
+ # admin_media_clicks.update_one(
2376
+ # {"_id": doc["_id"], "categories.categoryId": category_obj},
2377
+ # {
2378
+ # "$inc": {
2379
+ # "categories.$.click_count": 1,
2380
+ # "ai_edit_complete": 1, # $inc handles missing fields (backward compatible)
2381
+ # },
2382
+ # "$set": {
2383
+ # "categories.$.lastClickedAt": now,
2384
+ # "updatedAt": now,
2385
+ # "ai_edit_last_date": now,
2386
+ # "ai_edit_daily_count": updated_daily,
2387
+ # },
2388
+ # },
2389
+ # )
2390
+ # else:
2391
+ # # New category to existing document: push category, increment ai_edit_complete
2392
+ # admin_media_clicks.update_one(
2393
+ # {"_id": doc["_id"]},
2394
+ # {
2395
+ # "$push": {
2396
+ # "categories": {
2397
+ # "categoryId": category_obj,
2398
+ # "click_count": 1,
2399
+ # "lastClickedAt": now,
2400
+ # }
2401
+ # },
2402
+ # "$inc": {"ai_edit_complete": 1}, # $inc handles missing fields
2403
+ # "$set": {
2404
+ # "updatedAt": now,
2405
+ # "ai_edit_last_date": now,
2406
+ # "ai_edit_daily_count": updated_daily,
2407
+ # },
2408
+ # },
2409
+ # )
2410
+ # else:
2411
+ # # New user: create document with default ai_edit_complete=0, then increment to 1
2412
+ # daily_for_new = _build_ai_edit_daily_count(None, today)
2413
+ # admin_media_clicks.update_one(
2414
+ # {"userId": user_obj},
2415
+ # {
2416
+ # "$setOnInsert": {
2417
+ # "userId": user_obj,
2418
+ # "categories": [
2419
+ # {
2420
+ # "categoryId": category_obj,
2421
+ # "click_count": 1,
2422
+ # "lastClickedAt": now,
2423
+ # }
2424
+ # ],
2425
+ # "createdAt": now,
2426
+ # "updatedAt": now,
2427
+ # "ai_edit_daily_count": daily_for_new,
2428
+ # },
2429
+ # "$inc": {"ai_edit_complete": 1}, # Increment to 1 on first use
2430
+ # "$set": {
2431
+ # "updatedAt": now,
2432
+ # "ai_edit_last_date": now,
2433
+ # },
2434
+ # },
2435
+ # upsert=True,
2436
+ # )
2437
+ # except Exception as err:
2438
+ # err_str = str(err)
2439
+ # if "Unauthorized" in err_str or "not authorized" in err_str.lower():
2440
+ # log.warning(
2441
+ # "Admin media click logging failed (permissions): user lacks read/write on db=%s collection=%s. "
2442
+ # "Check MongoDB user permissions.",
2443
+ # admin_media_clicks.database.name,
2444
+ # admin_media_clicks.name,
2445
+ # )
2446
+ # else:
2447
+ # log.warning("Admin media click logging failed: %s", err)
2448
+
2449
+
2450
+ # @app.get("/")
2451
+ # def root() -> Dict[str, object]:
2452
+ # return {
2453
+ # "name": "Photo Object Removal API",
2454
+ # "status": "ok",
2455
+ # "endpoints": {
2456
+ # "GET /health": "health check",
2457
+ # "POST /upload-image": "form-data: image=file",
2458
+ # "POST /upload-mask": "form-data: mask=file",
2459
+ # "POST /inpaint": "JSON: {image_id, mask_id}",
2460
+ # "POST /inpaint-multipart": "form-data: image=file, mask=file",
2461
+ # "POST /remove-pink": "form-data: image=file (auto-detects pink segments and removes them)",
2462
+ # "GET /download/{filename}": "download result image",
2463
+ # "GET /result/{filename}": "view result image in browser",
2464
+ # "GET /logs": "recent uploads/results",
2465
+ # },
2466
+ # "auth": "set API_TOKEN env var to require Authorization: Bearer <token> (except /health)",
2467
+ # }
2468
+
2469
+
2470
+ # @app.get("/health")
2471
+ # def health() -> Dict[str, str]:
2472
+ # return {"status": "healthy"}
2473
+
2474
+
2475
+ # @app.get("/logging-status")
2476
+ # def logging_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
2477
+ # """Helper endpoint to verify admin media logging wiring (no secrets exposed)."""
2478
+ # return _admin_logging_status()
2479
+
2480
+
2481
+ # @app.post("/upload-image")
2482
+ # def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
2483
+ # ext = os.path.splitext(image.filename)[1] or ".png"
2484
+ # file_id = str(uuid.uuid4())
2485
+ # stored_name = f"{file_id}{ext}"
2486
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
2487
+ # with open(stored_path, "wb") as f:
2488
+ # shutil.copyfileobj(image.file, f)
2489
+ # file_store[file_id] = {
2490
+ # "type": "image",
2491
+ # "filename": image.filename,
2492
+ # "stored_name": stored_name,
2493
+ # "path": stored_path,
2494
+ # "timestamp": datetime.utcnow().isoformat(),
2495
+ # }
2496
+ # logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
2497
+ # return {"id": file_id, "filename": image.filename}
2498
+
2499
+
2500
+ # @app.post("/upload-mask")
2501
+ # def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
2502
+ # ext = os.path.splitext(mask.filename)[1] or ".png"
2503
+ # file_id = str(uuid.uuid4())
2504
+ # stored_name = f"{file_id}{ext}"
2505
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
2506
+ # with open(stored_path, "wb") as f:
2507
+ # shutil.copyfileobj(mask.file, f)
2508
+ # file_store[file_id] = {
2509
+ # "type": "mask",
2510
+ # "filename": mask.filename,
2511
+ # "stored_name": stored_name,
2512
+ # "path": stored_path,
2513
+ # "timestamp": datetime.utcnow().isoformat(),
2514
+ # }
2515
+ # logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
2516
+ # return {"id": file_id, "filename": mask.filename}
2517
+
2518
+
2519
+ # def _load_rgba_image(path: str) -> Image.Image:
2520
+ # img = Image.open(path)
2521
+ # return img.convert("RGBA")
2522
+
2523
+
2524
+ # def _compress_image(image_path: str, output_path: str, quality: int = 85) -> None:
2525
+ # """
2526
+ # Compress an image to reduce file size.
2527
+ # Converts to JPEG format with specified quality to achieve smaller file size.
2528
+ # """
2529
+ # img = Image.open(image_path)
2530
+ # # Convert RGBA to RGB if needed (JPEG doesn't support alpha)
2531
+ # if img.mode == "RGBA":
2532
+ # rgb_img = Image.new("RGB", img.size, (255, 255, 255))
2533
+ # rgb_img.paste(img, mask=img.split()[3]) # Use alpha channel as mask
2534
+ # img = rgb_img
2535
+ # elif img.mode != "RGB":
2536
+ # img = img.convert("RGB")
2537
+
2538
+ # # Save as JPEG with quality setting for compression
2539
+ # img.save(output_path, "JPEG", quality=quality, optimize=True)
2540
+
2541
+
2542
+ # def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
2543
+ # """
2544
+ # Convert mask image to RGBA format (black/white mask).
2545
+ # Standard convention: white (255) = area to remove, black (0) = area to keep
2546
+ # Returns RGBA with white in RGB channels where removal is needed, alpha=255
2547
+ # """
2548
+ # if img.mode != "RGBA":
2549
+ # # For RGB/Grayscale masks: white (value>128) = remove, black (value<=128) = keep
2550
+ # gray = img.convert("L")
2551
+ # arr = np.array(gray)
2552
+ # # Create proper black/white mask: white pixels (>128) = remove, black (<=128) = keep
2553
+ # mask_bw = np.where(arr > 128, 255, 0).astype(np.uint8)
2554
+
2555
+ # rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
2556
+ # rgba[:, :, 0] = mask_bw # R
2557
+ # rgba[:, :, 1] = mask_bw # G
2558
+ # rgba[:, :, 2] = mask_bw # B
2559
+ # rgba[:, :, 3] = 255 # Fully opaque
2560
+ # log.info(f"Loaded {img.mode} mask: {int((mask_bw > 0).sum())} white pixels (to remove)")
2561
+ # return rgba
2562
+
2563
+ # # For RGBA: check if alpha channel is meaningful
2564
+ # arr = np.array(img)
2565
+ # alpha = arr[:, :, 3]
2566
+ # rgb = arr[:, :, :3]
2567
+
2568
+ # # If alpha is mostly opaque everywhere (mean > 200), treat RGB channels as mask values
2569
+ # if alpha.mean() > 200:
2570
+ # # Use RGB to determine mask: white/bright in RGB = remove
2571
+ # gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
2572
+ # # Also detect magenta specifically
2573
+ # magenta = np.all(rgb == [255, 0, 255], axis=2).astype(np.uint8) * 255
2574
+ # mask_bw = np.maximum(np.where(gray > 128, 255, 0).astype(np.uint8), magenta)
2575
+
2576
+ # rgba = arr.copy()
2577
+ # rgba[:, :, 0] = mask_bw # R
2578
+ # rgba[:, :, 1] = mask_bw # G
2579
+ # rgba[:, :, 2] = mask_bw # B
2580
+ # rgba[:, :, 3] = 255 # Fully opaque
2581
+ # log.info(f"Loaded RGBA mask (RGB-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2582
+ # return rgba
2583
+
2584
+ # # Alpha channel encodes the mask - convert to RGB-based
2585
+ # # Transparent areas (alpha < 128) = remove, Opaque areas = keep
2586
+ # mask_bw = np.where(alpha < 128, 255, 0).astype(np.uint8)
2587
+ # rgba = arr.copy()
2588
+ # rgba[:, :, 0] = mask_bw
2589
+ # rgba[:, :, 1] = mask_bw
2590
+ # rgba[:, :, 2] = mask_bw
2591
+ # rgba[:, :, 3] = 255
2592
+ # log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2593
+ # return rgba
2594
+
2595
+ # @app.post("/inpaint")
2596
+ # def inpaint(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2597
+ # start_time = time.time()
2598
+ # status = "success"
2599
+ # error_msg = None
2600
+ # output_name = None
2601
+ # compressed_url = None
2602
+
2603
+ # try:
2604
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2605
+ # raise HTTPException(status_code=404, detail="image_id not found")
2606
+
2607
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2608
+ # raise HTTPException(status_code=404, detail="mask_id not found")
2609
+
2610
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2611
+ # mask_img = Image.open(file_store[req.mask_id]["path"])
2612
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2613
+
2614
+ # if req.passthrough:
2615
+ # result = np.array(img_rgba.convert("RGB"))
2616
+ # else:
2617
+ # result = process_inpaint(
2618
+ # np.array(img_rgba),
2619
+ # mask_rgba,
2620
+ # invert_mask=req.invert_mask
2621
+ # )
2622
+
2623
+ # output_name = f"output_{uuid.uuid4().hex}.png"
2624
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
2625
+
2626
+ # Image.fromarray(result).save(
2627
+ # output_path, "PNG", optimize=False, compress_level=1
2628
+ # )
2629
+
2630
+ # # Create compressed version
2631
+ # compressed_name = f"compressed_{output_name.replace('.png', '.jpg')}"
2632
+ # compressed_path = os.path.join(OUTPUT_DIR, compressed_name)
2633
+ # try:
2634
+ # _compress_image(output_path, compressed_path, quality=85)
2635
+ # compressed_url = str(request.url_for("download_file", filename=compressed_name))
2636
+ # except Exception as compress_err:
2637
+ # log.warning("Failed to create compressed image: %s", compress_err)
2638
+ # compressed_url = None
2639
+
2640
+ # log_media_click(req.user_id, req.category_id)
2641
+ # response = {"result": output_name}
2642
+ # if compressed_url:
2643
+ # response["Compressed_Image_URL"] = compressed_url
2644
+ # return response
2645
+
2646
+ # except Exception as e:
2647
+ # status = "fail"
2648
+ # error_msg = str(e)
2649
+ # raise
2650
+
2651
+ # finally:
2652
+ # end_time = time.time()
2653
+ # response_time_ms = (end_time - start_time) * 1000
2654
+
2655
+ # log_doc = {
2656
+ # "input_image_id": req.image_id,
2657
+ # "input_mask_id": req.mask_id,
2658
+ # "output_id": output_name,
2659
+ # "status": status,
2660
+ # "timestamp": datetime.utcnow(),
2661
+ # "ts": int(time.time()),
2662
+ # "response_time_ms": response_time_ms
2663
+ # }
2664
+
2665
+ # if error_msg:
2666
+ # log_doc["error"] = error_msg
2667
+
2668
+ # try:
2669
+ # mongo_logs.insert_one(log_doc)
2670
+ # except Exception as mongo_err:
2671
+ # log.error(f"Mongo log insert failed: {mongo_err}")
2672
+
2673
+ # # @app.post("/inpaint")
2674
+ # # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2675
+ # # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2676
+ # # raise HTTPException(status_code=404, detail="image_id not found")
2677
+ # # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2678
+ # # raise HTTPException(status_code=404, detail="mask_id not found")
2679
+
2680
+ # # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2681
+ # # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
2682
+ # # mask_rgba = _load_rgba_mask_from_image(mask_img)
2683
+
2684
+ # # # Debug: check mask before processing
2685
+ # # white_pixels = int((mask_rgba[:,:,0] > 128).sum())
2686
+ # # log.info(f"Inpaint request: mask has {white_pixels} white pixels, invert_mask={req.invert_mask}")
2687
+
2688
+ # # if req.passthrough:
2689
+ # # result = np.array(img_rgba.convert("RGB"))
2690
+ # # else:
2691
+ # # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
2692
+ # # result_name = f"output_{uuid.uuid4().hex}.png"
2693
+ # # result_path = os.path.join(OUTPUT_DIR, result_name)
2694
+ # # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2695
+
2696
+ # # logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
2697
+ # # return {"result": result_name}
2698
+
2699
+
2700
+ # @app.post("/inpaint-url")
2701
+ # def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2702
+ # """Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
2703
+ # start_time = time.time()
2704
+ # status = "success"
2705
+ # error_msg = None
2706
+ # result_name = None
2707
+
2708
+ # try:
2709
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2710
+ # raise HTTPException(status_code=404, detail="image_id not found")
2711
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2712
+ # raise HTTPException(status_code=404, detail="mask_id not found")
2713
+
2714
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2715
+ # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
2716
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2717
+
2718
+ # if req.passthrough:
2719
+ # result = np.array(img_rgba.convert("RGB"))
2720
+ # else:
2721
+ # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
2722
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2723
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2724
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2725
+
2726
+ # url = str(request.url_for("download_file", filename=result_name))
2727
+ # logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
2728
+ # log_media_click(req.user_id, req.category_id)
2729
+ # return {"result": result_name, "url": url}
2730
+ # except Exception as e:
2731
+ # status = "fail"
2732
+ # error_msg = str(e)
2733
+ # raise
2734
+ # finally:
2735
+ # # Always log to regular MongoDB (mandatory)
2736
+ # end_time = time.time()
2737
+ # response_time_ms = (end_time - start_time) * 1000
2738
+ # log_doc = {
2739
+ # "input_image_id": req.image_id,
2740
+ # "input_mask_id": req.mask_id,
2741
+ # "output_id": result_name,
2742
+ # "status": status,
2743
+ # "timestamp": datetime.utcnow(),
2744
+ # "ts": int(time.time()),
2745
+ # "response_time_ms": response_time_ms,
2746
+ # }
2747
+ # if error_msg:
2748
+ # log_doc["error"] = error_msg
2749
+ # try:
2750
+ # mongo_logs.insert_one(log_doc)
2751
+ # except Exception as mongo_err:
2752
+ # log.error("Mongo log insert failed: %s", mongo_err)
2753
+
2754
+
2755
+ # @app.post("/inpaint-multipart")
2756
+ # def inpaint_multipart(
2757
+ # image: UploadFile = File(...),
2758
+ # mask: UploadFile = File(...),
2759
+ # request: Request = None,
2760
+ # invert_mask: bool = True,
2761
+ # mask_is_painted: bool = False, # if True, mask file is the painted-on image (e.g., black strokes on original)
2762
+ # passthrough: bool = False,
2763
+ # user_id: Optional[str] = Form(None),
2764
+ # category_id: Optional[str] = Form(None),
2765
+ # _: None = Depends(bearer_auth),
2766
+ # ) -> Dict[str, str]:
2767
+ # start_time = time.time()
2768
+ # status = "success"
2769
+ # error_msg = None
2770
+ # result_name = None
2771
+
2772
+ # try:
2773
+ # # Load in-memory
2774
+ # img = Image.open(image.file).convert("RGBA")
2775
+ # m = Image.open(mask.file).convert("RGBA")
2776
+
2777
+ # if passthrough:
2778
+ # # Just echo the input image, ignore mask
2779
+ # result = np.array(img.convert("RGB"))
2780
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2781
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2782
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2783
+
2784
+ # url: Optional[str] = None
2785
+ # try:
2786
+ # if request is not None:
2787
+ # url = str(request.url_for("download_file", filename=result_name))
2788
+ # except Exception:
2789
+ # url = None
2790
+
2791
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2792
+ # if url:
2793
+ # entry["url"] = url
2794
+ # logs.append(entry)
2795
+ # resp: Dict[str, str] = {"result": result_name}
2796
+ # if url:
2797
+ # resp["url"] = url
2798
+ # log_media_click(user_id, category_id)
2799
+ # return resp
2800
+
2801
+ # if mask_is_painted:
2802
+ # # Auto-detect pink/magenta paint and convert to black/white mask
2803
+ # # White pixels = areas to remove, Black pixels = areas to keep
2804
+ # log.info("Auto-detecting pink/magenta paint from uploaded image...")
2805
+
2806
+ # m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
2807
+
2808
+ # # Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
2809
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2810
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2811
+ # magenta_detected = (
2812
+ # (m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
2813
+ # (m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
2814
+ # (m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
2815
+ # ).astype(np.uint8) * 255
2816
+
2817
+ # # Method 2: Also check if original image was provided to find differences
2818
+ # if img is not None:
2819
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2820
+ # if img_rgb.shape == m_rgb.shape:
2821
+ # diff = cv2.absdiff(img_rgb, m_rgb)
2822
+ # gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
2823
+ # # Any significant difference (>50) could be paint
2824
+ # diff_mask = (gray_diff > 50).astype(np.uint8) * 255
2825
+ # # Combine with magenta detection
2826
+ # binmask = cv2.bitwise_or(magenta_detected, diff_mask)
2827
+ # else:
2828
+ # binmask = magenta_detected
2829
+ # else:
2830
+ # # No original image provided, use magenta detection only
2831
+ # binmask = magenta_detected
2832
+
2833
+ # # Clean up the mask: remove noise and fill small holes
2834
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2835
+ # # Close small gaps in the mask
2836
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2837
+ # # Remove small noise
2838
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2839
+
2840
+ # nonzero = int((binmask > 0).sum())
2841
+ # log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
2842
+
2843
+ # # If very few pixels detected, assume the user may already be providing a BW mask
2844
+ # # and proceed without forcing strict detection
2845
+
2846
+ # if nonzero < 50:
2847
+ # log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
2848
+ # result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
2849
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2850
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2851
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2852
+ # return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
2853
+
2854
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
2855
+ # # Encode in RGBA format for process_inpaint
2856
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
2857
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
2858
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
2859
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
2860
+ # mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
2861
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
2862
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
2863
+ # # Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
2864
+ # mask_rgba[:, :, 3] = 255 - binmask
2865
+
2866
+ # log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
2867
+ # nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
2868
+ # else:
2869
+ # mask_rgba = _load_rgba_mask_from_image(m)
2870
+
2871
+ # # When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
2872
+ # actual_invert = invert_mask # Use default True for painted masks
2873
+ # log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
2874
+
2875
+ # result = process_inpaint(np.array(img), mask_rgba, invert_mask=actual_invert)
2876
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2877
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2878
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2879
+
2880
+ # url: Optional[str] = None
2881
+ # try:
2882
+ # if request is not None:
2883
+ # url = str(request.url_for("download_file", filename=result_name))
2884
+ # except Exception:
2885
+ # url = None
2886
+
2887
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2888
+ # if url:
2889
+ # entry["url"] = url
2890
+ # logs.append(entry)
2891
+ # resp: Dict[str, str] = {"result": result_name}
2892
+ # if url:
2893
+ # resp["url"] = url
2894
+ # log_media_click(user_id, category_id)
2895
+ # return resp
2896
+ # except Exception as e:
2897
+ # status = "fail"
2898
+ # error_msg = str(e)
2899
+ # raise
2900
+ # finally:
2901
+ # # Always log to regular MongoDB (mandatory)
2902
+ # end_time = time.time()
2903
+ # response_time_ms = (end_time - start_time) * 1000
2904
+ # log_doc = {
2905
+ # "endpoint": "inpaint-multipart",
2906
+ # "output_id": result_name,
2907
+ # "status": status,
2908
+ # "timestamp": datetime.utcnow(),
2909
+ # "ts": int(time.time()),
2910
+ # "response_time_ms": response_time_ms,
2911
+ # }
2912
+ # if error_msg:
2913
+ # log_doc["error"] = error_msg
2914
+ # try:
2915
+ # mongo_logs.insert_one(log_doc)
2916
+ # except Exception as mongo_err:
2917
+ # log.error("Mongo log insert failed: %s", mongo_err)
2918
+
2919
+
2920
+ # @app.post("/remove-pink")
2921
+ # def remove_pink_segments(
2922
+ # image: UploadFile = File(...),
2923
+ # request: Request = None,
2924
+ # user_id: Optional[str] = Form(None),
2925
+ # category_id: Optional[str] = Form(None),
2926
+ # _: None = Depends(bearer_auth),
2927
+ # ) -> Dict[str, str]:
2928
+ # """
2929
+ # Simple endpoint: upload an image with pink/magenta segments to remove.
2930
+ # - Pink/Magenta segments → automatically removed (white in mask)
2931
+ # - Everything else → automatically kept (black in mask)
2932
+ # Just paint pink/magenta on areas you want to remove, upload the image, and it works!
2933
+ # """
2934
+ # start_time = time.time()
2935
+ # status = "success"
2936
+ # error_msg = None
2937
+ # result_name = None
2938
+
2939
+ # try:
2940
+ # log.info(f"Simple remove-pink: processing image {image.filename}")
2941
+
2942
+ # # Load the image (with pink paint on it)
2943
+ # img = Image.open(image.file).convert("RGBA")
2944
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2945
+
2946
+ # # Auto-detect pink/magenta segments to remove
2947
+ # # Pink/Magenta → white in mask (remove)
2948
+ # # Everything else (natural image colors, including dark areas) → black in mask (keep)
2949
+
2950
+ # # Detect pink/magenta using fixed RGB bounds per requested logic
2951
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2952
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2953
+ # binmask = (
2954
+ # (img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
2955
+ # (img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
2956
+ # (img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
2957
+ # ).astype(np.uint8) * 255
2958
+
2959
+ # # Clean up the pink mask
2960
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2961
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2962
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2963
+
2964
+ # nonzero = int((binmask > 0).sum())
2965
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
2966
+ # log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
2967
+
2968
+ # # Debug: log bounds used
2969
+ # log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
2970
+
2971
+ # if nonzero < 50:
2972
+ # log.error("No pink segments detected! Returning original image.")
2973
+ # result = np.array(img.convert("RGB"))
2974
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2975
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2976
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2977
+ # return {
2978
+ # "result": result_name,
2979
+ # "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
2980
+ # }
2981
+
2982
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
2983
+ # # Encode in RGBA format that process_inpaint expects
2984
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
2985
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
2986
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
2987
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
2988
+ # # RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
2989
+ # mask_rgba[:, :, 0] = binmask # R: white where pink
2990
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
2991
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
2992
+ # # Alpha: 0 (transparent) where pink → will become white after 255-alpha
2993
+ # # 255 (opaque) everywhere else → will become black after 255-alpha
2994
+ # mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
2995
+
2996
+ # # Verify mask encoding
2997
+ # alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
2998
+ # alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
2999
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
3000
+ # log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
3001
+ # log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
3002
+
3003
+ # # IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
3004
+ # # Remove pink from the original image before processing
3005
+ # # Create a clean version: where pink was detected, keep original image colors
3006
+ # img_clean = np.array(img.convert("RGBA"))
3007
+ # # Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
3008
+ # # Actually, the model will inpaint over those areas, so we can pass the original
3009
+ # # But for better results, we might want to remove the pink overlay first
3010
+
3011
+ # # Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
3012
+ # log.info(f"Starting inpainting process...")
3013
+ # result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
3014
+ # log.info(f"Inpainting complete, result shape: {result.shape}")
3015
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3016
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3017
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3018
+
3019
+ # url: Optional[str] = None
3020
+ # try:
3021
+ # if request is not None:
3022
+ # url = str(request.url_for("download_file", filename=result_name))
3023
+ # except Exception:
3024
+ # url = None
3025
+
3026
+ # logs.append({
3027
+ # "result": result_name,
3028
+ # "filename": image.filename,
3029
+ # "pink_pixels": nonzero,
3030
+ # "timestamp": datetime.utcnow().isoformat()
3031
+ # })
3032
+
3033
+ # resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
3034
+ # if url:
3035
+ # resp["url"] = url
3036
+ # log_media_click(user_id, category_id)
3037
+ # return resp
3038
+ # except Exception as e:
3039
+ # status = "fail"
3040
+ # error_msg = str(e)
3041
+ # raise
3042
+ # finally:
3043
+ # # Always log to regular MongoDB (mandatory)
3044
+ # end_time = time.time()
3045
+ # response_time_ms = (end_time - start_time) * 1000
3046
+ # log_doc = {
3047
+ # "endpoint": "remove-pink",
3048
+ # "output_id": result_name,
3049
+ # "status": status,
3050
+ # "timestamp": datetime.utcnow(),
3051
+ # "ts": int(time.time()),
3052
+ # "response_time_ms": response_time_ms,
3053
+ # }
3054
+ # if error_msg:
3055
+ # log_doc["error"] = error_msg
3056
+ # try:
3057
+ # mongo_logs.insert_one(log_doc)
3058
+ # except Exception as mongo_err:
3059
+ # log.error("Mongo log insert failed: %s", mongo_err)
3060
+
3061
+
3062
+ # @app.get("/download/{filename}")
3063
+ # def download_file(filename: str):
3064
+ # path = os.path.join(OUTPUT_DIR, filename)
3065
+ # if not os.path.isfile(path):
3066
+ # raise HTTPException(status_code=404, detail="file not found")
3067
+ # return FileResponse(path)
3068
+
3069
+
3070
+ # @app.get("/result/{filename}")
3071
+ # def view_result(filename: str):
3072
+ # """View result image directly in browser (same as download but with proper content-type for viewing)"""
3073
+ # path = os.path.join(OUTPUT_DIR, filename)
3074
+ # if not os.path.isfile(path):
3075
+ # raise HTTPException(status_code=404, detail="file not found")
3076
+ # return FileResponse(path, media_type="image/png")
3077
+
3078
+
3079
+ # @app.get("/logs")
3080
+ # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
3081
+ # return JSONResponse(content=logs)