LogicGoInfotechSpaces commited on
Commit
8a43ca9
·
verified ·
1 Parent(s): 55d4bd5

Update api/main.py

Browse files
Files changed (1) hide show
  1. api/main.py +968 -7
api/main.py CHANGED
@@ -1,5 +1,4 @@
1
 
2
-
3
  import os
4
  import uuid
5
  import shutil
@@ -639,11 +638,14 @@ Final result must look clean, realistic, and object-free."""
639
  img_bytes = io.BytesIO()
640
  img_rgb.save(img_bytes, format='PNG')
641
  img_bytes.seek(0)
 
642
 
643
  # Convert mask to bytes
 
644
  mask_bytes = io.BytesIO()
645
- mask_img.save(mask_bytes, format='PNG')
646
  mask_bytes.seek(0)
 
647
 
648
  # Use Gemini 2.5 Flash Image model for image processing
649
  model = None
@@ -694,15 +696,24 @@ Final result must look clean, realistic, and object-free."""
694
  except Exception as list_err:
695
  log.warning("Could not list models: %s", list_err)
696
 
697
- # Prepare image parts for Gemini API
 
 
 
 
 
698
  image_part = {
699
- "mime_type": "image/png",
700
- "data": img_bytes.read()
 
 
701
  }
702
 
703
  mask_part = {
704
- "mime_type": "image/png",
705
- "data": mask_bytes.read()
 
 
706
  }
707
 
708
  # Generate inpainting using Gemini 2.5 Flash Image API with the specified prompt
@@ -3533,6 +3544,956 @@ def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
3533
  # return img.convert("RGBA")
3534
 
3535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3536
  # def _compress_image(image_path: str, output_path: str, quality: int = 85) -> None:
3537
  # """
3538
  # Compress an image to reduce file size.
 
1
 
 
2
  import os
3
  import uuid
4
  import shutil
 
638
  img_bytes = io.BytesIO()
639
  img_rgb.save(img_bytes, format='PNG')
640
  img_bytes.seek(0)
641
+ img_data = img_bytes.read()
642
 
643
  # Convert mask to bytes
644
+ mask_rgb = mask_img.convert("RGB") if mask_img.mode != "RGB" else mask_img
645
  mask_bytes = io.BytesIO()
646
+ mask_rgb.save(mask_bytes, format='PNG')
647
  mask_bytes.seek(0)
648
+ mask_data = mask_bytes.read()
649
 
650
  # Use Gemini 2.5 Flash Image model for image processing
651
  model = None
 
696
  except Exception as list_err:
697
  log.warning("Could not list models: %s", list_err)
698
 
699
+ # Prepare image parts for Gemini API using proper Part format
700
+ # Convert bytes to base64 for inline_data format
701
+ img_base64 = base64.b64encode(img_data).decode('utf-8')
702
+ mask_base64 = base64.b64encode(mask_data).decode('utf-8')
703
+
704
+ # Use the proper inline_data format
705
  image_part = {
706
+ "inline_data": {
707
+ "mime_type": "image/png",
708
+ "data": img_base64
709
+ }
710
  }
711
 
712
  mask_part = {
713
+ "inline_data": {
714
+ "mime_type": "image/png",
715
+ "data": mask_base64
716
+ }
717
  }
718
 
719
  # Generate inpainting using Gemini 2.5 Flash Image API with the specified prompt
 
3544
  # return img.convert("RGBA")
3545
 
3546
 
3547
+ # def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
3548
+ # """
3549
+ # Convert mask image to RGBA format (black/white mask).
3550
+ # Standard convention: white (255) = area to remove, black (0) = area to keep
3551
+ # Returns RGBA with white in RGB channels where removal is needed, alpha=255
3552
+ # """
3553
+ # if img.mode != "RGBA":
3554
+ # # For RGB/Grayscale masks: white (value>128) = remove, black (value<=128) = keep
3555
+ # gray = img.convert("L")
3556
+ # arr = np.array(gray)
3557
+ # # Create proper black/white mask: white pixels (>128) = remove, black (<=128) = keep
3558
+ # mask_bw = np.where(arr > 128, 255, 0).astype(np.uint8)
3559
+
3560
+ # rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
3561
+ # rgba[:, :, 0] = mask_bw # R
3562
+ # rgba[:, :, 1] = mask_bw # G
3563
+ # rgba[:, :, 2] = mask_bw # B
3564
+ # rgba[:, :, 3] = 255 # Fully opaque
3565
+ # log.info(f"Loaded {img.mode} mask: {int((mask_bw > 0).sum())} white pixels (to remove)")
3566
+ # return rgba
3567
+
3568
+ # # For RGBA: check if alpha channel is meaningful
3569
+ # arr = np.array(img)
3570
+ # alpha = arr[:, :, 3]
3571
+ # rgb = arr[:, :, :3]
3572
+
3573
+ # # If alpha is mostly opaque everywhere (mean > 200), treat RGB channels as mask values
3574
+ # if alpha.mean() > 200:
3575
+ # # Use RGB to determine mask: white/bright in RGB = remove
3576
+ # gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
3577
+ # # Also detect magenta specifically
3578
+ # magenta = np.all(rgb == [255, 0, 255], axis=2).astype(np.uint8) * 255
3579
+ # mask_bw = np.maximum(np.where(gray > 128, 255, 0).astype(np.uint8), magenta)
3580
+
3581
+ # rgba = arr.copy()
3582
+ # rgba[:, :, 0] = mask_bw # R
3583
+ # rgba[:, :, 1] = mask_bw # G
3584
+ # rgba[:, :, 2] = mask_bw # B
3585
+ # rgba[:, :, 3] = 255 # Fully opaque
3586
+ # log.info(f"Loaded RGBA mask (RGB-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
3587
+ # return rgba
3588
+
3589
+ # # Alpha channel encodes the mask - convert to RGB-based
3590
+ # # Transparent areas (alpha < 128) = remove, Opaque areas = keep
3591
+ # mask_bw = np.where(alpha < 128, 255, 0).astype(np.uint8)
3592
+ # rgba = arr.copy()
3593
+ # rgba[:, :, 0] = mask_bw
3594
+ # rgba[:, :, 1] = mask_bw
3595
+ # rgba[:, :, 2] = mask_bw
3596
+ # rgba[:, :, 3] = 255
3597
+ # log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
3598
+ # return rgba
3599
+
3600
+ # @app.post("/inpaint")
3601
+ # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
3602
+ # start_time = time.time()
3603
+ # status = "success"
3604
+ # error_msg = None
3605
+ # output_name = None
3606
+
3607
+ # try:
3608
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
3609
+ # raise HTTPException(status_code=404, detail="image_id not found")
3610
+
3611
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
3612
+ # raise HTTPException(status_code=404, detail="mask_id not found")
3613
+
3614
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
3615
+ # mask_img = Image.open(file_store[req.mask_id]["path"])
3616
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
3617
+
3618
+ # if req.passthrough:
3619
+ # result = np.array(img_rgba.convert("RGB"))
3620
+ # else:
3621
+ # result = process_inpaint(
3622
+ # np.array(img_rgba),
3623
+ # mask_rgba,
3624
+ # invert_mask=req.invert_mask
3625
+ # )
3626
+
3627
+ # output_name = f"output_{uuid.uuid4().hex}.png"
3628
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
3629
+
3630
+ # Image.fromarray(result).save(
3631
+ # output_path, "PNG", optimize=False, compress_level=1
3632
+ # )
3633
+
3634
+ # log_media_click(req.user_id, req.category_id)
3635
+ # return {"result": output_name}
3636
+
3637
+ # except Exception as e:
3638
+ # status = "fail"
3639
+ # error_msg = str(e)
3640
+ # raise
3641
+
3642
+ # finally:
3643
+ # end_time = time.time()
3644
+ # response_time_ms = (end_time - start_time) * 1000
3645
+
3646
+ # log_doc = {
3647
+ # "input_image_id": req.image_id,
3648
+ # "input_mask_id": req.mask_id,
3649
+ # "output_id": output_name,
3650
+ # "status": status,
3651
+ # "timestamp": datetime.utcnow(),
3652
+ # "ts": int(time.time()),
3653
+ # "response_time_ms": response_time_ms
3654
+ # }
3655
+
3656
+ # if error_msg:
3657
+ # log_doc["error"] = error_msg
3658
+
3659
+ # try:
3660
+ # mongo_logs.insert_one(log_doc)
3661
+ # except Exception as mongo_err:
3662
+ # log.error(f"Mongo log insert failed: {mongo_err}")
3663
+
3664
+ # # @app.post("/inpaint")
3665
+ # # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
3666
+ # # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
3667
+ # # raise HTTPException(status_code=404, detail="image_id not found")
3668
+ # # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
3669
+ # # raise HTTPException(status_code=404, detail="mask_id not found")
3670
+
3671
+ # # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
3672
+ # # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
3673
+ # # mask_rgba = _load_rgba_mask_from_image(mask_img)
3674
+
3675
+ # # # Debug: check mask before processing
3676
+ # # white_pixels = int((mask_rgba[:,:,0] > 128).sum())
3677
+ # # log.info(f"Inpaint request: mask has {white_pixels} white pixels, invert_mask={req.invert_mask}")
3678
+
3679
+ # # if req.passthrough:
3680
+ # # result = np.array(img_rgba.convert("RGB"))
3681
+ # # else:
3682
+ # # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
3683
+ # # result_name = f"output_{uuid.uuid4().hex}.png"
3684
+ # # result_path = os.path.join(OUTPUT_DIR, result_name)
3685
+ # # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3686
+
3687
+ # # logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
3688
+ # # return {"result": result_name}
3689
+
3690
+
3691
+ # @app.post("/inpaint-url")
3692
+ # def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
3693
+ # """Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
3694
+ # start_time = time.time()
3695
+ # status = "success"
3696
+ # error_msg = None
3697
+ # result_name = None
3698
+
3699
+ # try:
3700
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
3701
+ # raise HTTPException(status_code=404, detail="image_id not found")
3702
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
3703
+ # raise HTTPException(status_code=404, detail="mask_id not found")
3704
+
3705
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
3706
+ # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
3707
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
3708
+
3709
+ # if req.passthrough:
3710
+ # result = np.array(img_rgba.convert("RGB"))
3711
+ # else:
3712
+ # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
3713
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3714
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3715
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3716
+
3717
+ # url = str(request.url_for("download_file", filename=result_name))
3718
+ # logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
3719
+ # log_media_click(req.user_id, req.category_id)
3720
+ # return {"result": result_name, "url": url}
3721
+ # except Exception as e:
3722
+ # status = "fail"
3723
+ # error_msg = str(e)
3724
+ # raise
3725
+ # finally:
3726
+ # # Always log to regular MongoDB (mandatory)
3727
+ # end_time = time.time()
3728
+ # response_time_ms = (end_time - start_time) * 1000
3729
+ # log_doc = {
3730
+ # "input_image_id": req.image_id,
3731
+ # "input_mask_id": req.mask_id,
3732
+ # "output_id": result_name,
3733
+ # "status": status,
3734
+ # "timestamp": datetime.utcnow(),
3735
+ # "ts": int(time.time()),
3736
+ # "response_time_ms": response_time_ms,
3737
+ # }
3738
+ # if error_msg:
3739
+ # log_doc["error"] = error_msg
3740
+ # try:
3741
+ # mongo_logs.insert_one(log_doc)
3742
+ # except Exception as mongo_err:
3743
+ # log.error("Mongo log insert failed: %s", mongo_err)
3744
+
3745
+
3746
+ # @app.post("/inpaint-multipart")
3747
+ # def inpaint_multipart(
3748
+ # image: UploadFile = File(...),
3749
+ # mask: UploadFile = File(...),
3750
+ # request: Request = None,
3751
+ # invert_mask: bool = True,
3752
+ # mask_is_painted: bool = False, # if True, mask file is the painted-on image (e.g., black strokes on original)
3753
+ # passthrough: bool = False,
3754
+ # user_id: Optional[str] = Form(None),
3755
+ # category_id: Optional[str] = Form(None),
3756
+ # _: None = Depends(bearer_auth),
3757
+ # ) -> Dict[str, str]:
3758
+ # start_time = time.time()
3759
+ # status = "success"
3760
+ # error_msg = None
3761
+ # result_name = None
3762
+
3763
+ # try:
3764
+ # # Load in-memory
3765
+ # img = Image.open(image.file).convert("RGBA")
3766
+ # m = Image.open(mask.file).convert("RGBA")
3767
+
3768
+ # if passthrough:
3769
+ # # Just echo the input image, ignore mask
3770
+ # result = np.array(img.convert("RGB"))
3771
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3772
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3773
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3774
+
3775
+ # url: Optional[str] = None
3776
+ # try:
3777
+ # if request is not None:
3778
+ # url = str(request.url_for("download_file", filename=result_name))
3779
+ # except Exception:
3780
+ # url = None
3781
+
3782
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
3783
+ # if url:
3784
+ # entry["url"] = url
3785
+ # logs.append(entry)
3786
+ # resp: Dict[str, str] = {"result": result_name}
3787
+ # if url:
3788
+ # resp["url"] = url
3789
+ # log_media_click(user_id, category_id)
3790
+ # return resp
3791
+
3792
+ # if mask_is_painted:
3793
+ # # Auto-detect pink/magenta paint and convert to black/white mask
3794
+ # # White pixels = areas to remove, Black pixels = areas to keep
3795
+ # log.info("Auto-detecting pink/magenta paint from uploaded image...")
3796
+
3797
+ # m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
3798
+
3799
+ # # Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
3800
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
3801
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
3802
+ # magenta_detected = (
3803
+ # (m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
3804
+ # (m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
3805
+ # (m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
3806
+ # ).astype(np.uint8) * 255
3807
+
3808
+ # # Method 2: Also check if original image was provided to find differences
3809
+ # if img is not None:
3810
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
3811
+ # if img_rgb.shape == m_rgb.shape:
3812
+ # diff = cv2.absdiff(img_rgb, m_rgb)
3813
+ # gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
3814
+ # # Any significant difference (>50) could be paint
3815
+ # diff_mask = (gray_diff > 50).astype(np.uint8) * 255
3816
+ # # Combine with magenta detection
3817
+ # binmask = cv2.bitwise_or(magenta_detected, diff_mask)
3818
+ # else:
3819
+ # binmask = magenta_detected
3820
+ # else:
3821
+ # # No original image provided, use magenta detection only
3822
+ # binmask = magenta_detected
3823
+
3824
+ # # Clean up the mask: remove noise and fill small holes
3825
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
3826
+ # # Close small gaps in the mask
3827
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
3828
+ # # Remove small noise
3829
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
3830
+
3831
+ # nonzero = int((binmask > 0).sum())
3832
+ # log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
3833
+
3834
+ # # If very few pixels detected, assume the user may already be providing a BW mask
3835
+ # # and proceed without forcing strict detection
3836
+
3837
+ # if nonzero < 50:
3838
+ # log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
3839
+ # result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
3840
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3841
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3842
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3843
+ # return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
3844
+
3845
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
3846
+ # # Encode in RGBA format for process_inpaint
3847
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
3848
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
3849
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
3850
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
3851
+ # mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
3852
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
3853
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
3854
+ # # Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
3855
+ # mask_rgba[:, :, 3] = 255 - binmask
3856
+
3857
+ # log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
3858
+ # nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
3859
+ # else:
3860
+ # mask_rgba = _load_rgba_mask_from_image(m)
3861
+
3862
+ # # When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
3863
+ # actual_invert = invert_mask # Use default True for painted masks
3864
+ # log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
3865
+
3866
+ # result = process_inpaint(np.array(img), mask_rgba, invert_mask=actual_invert)
3867
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3868
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3869
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3870
+
3871
+ # url: Optional[str] = None
3872
+ # try:
3873
+ # if request is not None:
3874
+ # url = str(request.url_for("download_file", filename=result_name))
3875
+ # except Exception:
3876
+ # url = None
3877
+
3878
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
3879
+ # if url:
3880
+ # entry["url"] = url
3881
+ # logs.append(entry)
3882
+ # resp: Dict[str, str] = {"result": result_name}
3883
+ # if url:
3884
+ # resp["url"] = url
3885
+ # log_media_click(user_id, category_id)
3886
+ # return resp
3887
+ # except Exception as e:
3888
+ # status = "fail"
3889
+ # error_msg = str(e)
3890
+ # raise
3891
+ # finally:
3892
+ # # Always log to regular MongoDB (mandatory)
3893
+ # end_time = time.time()
3894
+ # response_time_ms = (end_time - start_time) * 1000
3895
+ # log_doc = {
3896
+ # "endpoint": "inpaint-multipart",
3897
+ # "output_id": result_name,
3898
+ # "status": status,
3899
+ # "timestamp": datetime.utcnow(),
3900
+ # "ts": int(time.time()),
3901
+ # "response_time_ms": response_time_ms,
3902
+ # }
3903
+ # if error_msg:
3904
+ # log_doc["error"] = error_msg
3905
+ # try:
3906
+ # mongo_logs.insert_one(log_doc)
3907
+ # except Exception as mongo_err:
3908
+ # log.error("Mongo log insert failed: %s", mongo_err)
3909
+
3910
+
3911
+ # @app.post("/remove-pink")
3912
+ # def remove_pink_segments(
3913
+ # image: UploadFile = File(...),
3914
+ # request: Request = None,
3915
+ # user_id: Optional[str] = Form(None),
3916
+ # category_id: Optional[str] = Form(None),
3917
+ # _: None = Depends(bearer_auth),
3918
+ # ) -> Dict[str, str]:
3919
+ # """
3920
+ # Simple endpoint: upload an image with pink/magenta segments to remove.
3921
+ # - Pink/Magenta segments → automatically removed (white in mask)
3922
+ # - Everything else → automatically kept (black in mask)
3923
+ # Just paint pink/magenta on areas you want to remove, upload the image, and it works!
3924
+ # """
3925
+ # start_time = time.time()
3926
+ # status = "success"
3927
+ # error_msg = None
3928
+ # result_name = None
3929
+
3930
+ # try:
3931
+ # log.info(f"Simple remove-pink: processing image {image.filename}")
3932
+
3933
+ # # Load the image (with pink paint on it)
3934
+ # img = Image.open(image.file).convert("RGBA")
3935
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
3936
+
3937
+ # # Auto-detect pink/magenta segments to remove
3938
+ # # Pink/Magenta → white in mask (remove)
3939
+ # # Everything else (natural image colors, including dark areas) → black in mask (keep)
3940
+
3941
+ # # Detect pink/magenta using fixed RGB bounds per requested logic
3942
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
3943
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
3944
+ # binmask = (
3945
+ # (img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
3946
+ # (img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
3947
+ # (img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
3948
+ # ).astype(np.uint8) * 255
3949
+
3950
+ # # Clean up the pink mask
3951
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
3952
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
3953
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
3954
+
3955
+ # nonzero = int((binmask > 0).sum())
3956
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
3957
+ # log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
3958
+
3959
+ # # Debug: log bounds used
3960
+ # log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
3961
+
3962
+ # if nonzero < 50:
3963
+ # log.error("No pink segments detected! Returning original image.")
3964
+ # result = np.array(img.convert("RGB"))
3965
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3966
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3967
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3968
+ # return {
3969
+ # "result": result_name,
3970
+ # "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
3971
+ # }
3972
+
3973
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
3974
+ # # Encode in RGBA format that process_inpaint expects
3975
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
3976
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
3977
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
3978
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
3979
+ # # RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
3980
+ # mask_rgba[:, :, 0] = binmask # R: white where pink
3981
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
3982
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
3983
+ # # Alpha: 0 (transparent) where pink → will become white after 255-alpha
3984
+ # # 255 (opaque) everywhere else → will become black after 255-alpha
3985
+ # mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
3986
+
3987
+ # # Verify mask encoding
3988
+ # alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
3989
+ # alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
3990
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
3991
+ # log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
3992
+ # log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
3993
+
3994
+ # # IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
3995
+ # # Remove pink from the original image before processing
3996
+ # # Create a clean version: where pink was detected, keep original image colors
3997
+ # img_clean = np.array(img.convert("RGBA"))
3998
+ # # Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
3999
+ # # Actually, the model will inpaint over those areas, so we can pass the original
4000
+ # # But for better results, we might want to remove the pink overlay first
4001
+
4002
+ # # Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
4003
+ # log.info(f"Starting inpainting process...")
4004
+ # result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
4005
+ # log.info(f"Inpainting complete, result shape: {result.shape}")
4006
+ # result_name = f"output_{uuid.uuid4().hex}.png"
4007
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
4008
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
4009
+
4010
+ # url: Optional[str] = None
4011
+ # try:
4012
+ # if request is not None:
4013
+ # url = str(request.url_for("download_file", filename=result_name))
4014
+ # except Exception:
4015
+ # url = None
4016
+
4017
+ # logs.append({
4018
+ # "result": result_name,
4019
+ # "filename": image.filename,
4020
+ # "pink_pixels": nonzero,
4021
+ # "timestamp": datetime.utcnow().isoformat()
4022
+ # })
4023
+
4024
+ # resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
4025
+ # if url:
4026
+ # resp["url"] = url
4027
+ # log_media_click(user_id, category_id)
4028
+ # return resp
4029
+ # except Exception as e:
4030
+ # status = "fail"
4031
+ # error_msg = str(e)
4032
+ # raise
4033
+ # finally:
4034
+ # # Always log to regular MongoDB (mandatory)
4035
+ # end_time = time.time()
4036
+ # response_time_ms = (end_time - start_time) * 1000
4037
+ # log_doc = {
4038
+ # "endpoint": "remove-pink",
4039
+ # "output_id": result_name,
4040
+ # "status": status,
4041
+ # "timestamp": datetime.utcnow(),
4042
+ # "ts": int(time.time()),
4043
+ # "response_time_ms": response_time_ms,
4044
+ # }
4045
+ # if error_msg:
4046
+ # log_doc["error"] = error_msg
4047
+ # try:
4048
+ # mongo_logs.insert_one(log_doc)
4049
+ # except Exception as mongo_err:
4050
+ # log.error("Mongo log insert failed: %s", mongo_err)
4051
+
4052
+
4053
+ # @app.get("/download/{filename}")
4054
+ # def download_file(filename: str):
4055
+ # path = os.path.join(OUTPUT_DIR, filename)
4056
+ # if not os.path.isfile(path):
4057
+ # raise HTTPException(status_code=404, detail="file not found")
4058
+ # return FileResponse(path)
4059
+
4060
+
4061
+ # @app.get("/result/{filename}")
4062
+ # def view_result(filename: str):
4063
+ # """View result image directly in browser (same as download but with proper content-type for viewing)"""
4064
+ # path = os.path.join(OUTPUT_DIR, filename)
4065
+ # if not os.path.isfile(path):
4066
+ # raise HTTPException(status_code=404, detail="file not found")
4067
+ # return FileResponse(path, media_type="image/png")
4068
+
4069
+
4070
+ # @app.get("/logs")
4071
+ # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
4072
+ # return JSONResponse(content=logs)
4073
+ # import os
4074
+ # import uuid
4075
+ # import shutil
4076
+ # import re
4077
+ # from datetime import datetime, timedelta, date
4078
+ # from typing import Dict, List, Optional
4079
+
4080
+ # import numpy as np
4081
+ # from fastapi import (
4082
+ # FastAPI,
4083
+ # UploadFile,
4084
+ # File,
4085
+ # HTTPException,
4086
+ # Depends,
4087
+ # Header,
4088
+ # Request,
4089
+ # Form,
4090
+ # )
4091
+ # from fastapi.responses import FileResponse, JSONResponse
4092
+ # from pydantic import BaseModel
4093
+ # from PIL import Image
4094
+ # import cv2
4095
+ # import logging
4096
+
4097
+ # from bson import ObjectId
4098
+ # from pymongo import MongoClient
4099
+ # import time
4100
+
4101
+ # logging.basicConfig(level=logging.INFO)
4102
+ # log = logging.getLogger("api")
4103
+
4104
+ # from src.core import process_inpaint
4105
+
4106
+ # # Directories (use writable space on HF Spaces)
4107
+ # BASE_DIR = os.environ.get("DATA_DIR", "/data")
4108
+ # if not os.path.isdir(BASE_DIR):
4109
+ # # Fallback to /tmp if /data not available
4110
+ # BASE_DIR = "/tmp"
4111
+
4112
+ # UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
4113
+ # OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
4114
+
4115
+ # os.makedirs(UPLOAD_DIR, exist_ok=True)
4116
+ # os.makedirs(OUTPUT_DIR, exist_ok=True)
4117
+
4118
+ # # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
4119
+ # ENV_TOKEN = os.environ.get("API_TOKEN")
4120
+
4121
+ # app = FastAPI(title="Photo Object Removal API", version="1.0.0")
4122
+
4123
+ # # In-memory stores
4124
+ # file_store: Dict[str, Dict[str, str]] = {}
4125
+ # logs: List[Dict[str, str]] = []
4126
+
4127
+ # MONGO_URI = "mongodb+srv://harilogicgo_db_user:pdnh6UCMsWvuTCoi@kiddoimages.k2a4nuv.mongodb.net/?appName=KiddoImages"
4128
+ # mongo_client = MongoClient(MONGO_URI)
4129
+ # mongo_db = mongo_client["object_remover"]
4130
+ # mongo_logs = mongo_db["api_logs"]
4131
+
4132
+ # ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
4133
+ # DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
4134
+ # admin_media_clicks = None
4135
+
4136
+
4137
+ # def _init_admin_mongo() -> None:
4138
+ # global admin_media_clicks
4139
+ # if not ADMIN_MONGO_URI:
4140
+ # log.info("Admin Mongo URI not provided; media click logging disabled")
4141
+ # return
4142
+ # try:
4143
+ # admin_client = MongoClient(ADMIN_MONGO_URI)
4144
+ # # get_default_database() extracts database from connection string (e.g., /adminPanel)
4145
+ # admin_db = admin_client.get_default_database()
4146
+ # if admin_db is None:
4147
+ # # Fallback if no database in URI
4148
+ # admin_db = admin_client["admin"]
4149
+ # log.warning("No database in connection string, defaulting to 'admin'")
4150
+
4151
+ # admin_media_clicks = admin_db["media_clicks"]
4152
+ # log.info(
4153
+ # "Admin media click logging initialized: db=%s collection=%s",
4154
+ # admin_db.name,
4155
+ # admin_media_clicks.name,
4156
+ # )
4157
+ # try:
4158
+ # admin_media_clicks.drop_index("user_id_1_header_1_media_id_1")
4159
+ # log.info("Dropped legacy index user_id_1_header_1_media_id_1")
4160
+ # except Exception as idx_err:
4161
+ # # Index drop failure is non-critical (often permission issue)
4162
+ # if "Unauthorized" not in str(idx_err):
4163
+ # log.info("Skipping legacy index drop: %s", idx_err)
4164
+ # except Exception as err:
4165
+ # log.error("Failed to init admin Mongo client: %s", err)
4166
+ # admin_media_clicks = None
4167
+
4168
+
4169
+ # _init_admin_mongo()
4170
+
4171
+
4172
+ # def _admin_logging_status() -> Dict[str, object]:
4173
+ # if admin_media_clicks is None:
4174
+ # return {
4175
+ # "enabled": False,
4176
+ # "db": None,
4177
+ # "collection": None,
4178
+ # }
4179
+ # return {
4180
+ # "enabled": True,
4181
+ # "db": admin_media_clicks.database.name,
4182
+ # "collection": admin_media_clicks.name,
4183
+ # }
4184
+
4185
+
4186
+ # def _build_ai_edit_daily_count(
4187
+ # existing: Optional[List[Dict[str, object]]],
4188
+ # today: date,
4189
+ # ) -> List[Dict[str, object]]:
4190
+ # """
4191
+ # Build / extend the ai_edit_daily_count array with the following rules:
4192
+
4193
+ # - Case A (no existing data): return [{date: today, count: 1}]
4194
+ # - Case B (today already recorded): return list unchanged
4195
+ # - Case C (gap in days): fill missing days with count=0 and append today with count=1
4196
+
4197
+ # Additionally, the returned list is capped to the most recent 32 entries.
4198
+
4199
+ # The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
4200
+ # """
4201
+
4202
+ # def _to_date_only(value: object) -> date:
4203
+ # if isinstance(value, datetime):
4204
+ # return value.date()
4205
+ # if isinstance(value, date):
4206
+ # return value
4207
+ # # Fallback: try parsing ISO string "YYYY-MM-DD" or full datetime
4208
+ # try:
4209
+ # text = str(value)
4210
+ # if len(text) == 10:
4211
+ # return datetime.strptime(text, "%Y-%m-%d").date()
4212
+ # return datetime.fromisoformat(text).date()
4213
+ # except Exception:
4214
+ # # If parsing fails, just treat as today to avoid crashing
4215
+ # return today
4216
+
4217
+ # # Case A: first ever use (no array yet)
4218
+ # if not existing:
4219
+ # return [
4220
+ # {
4221
+ # "date": datetime(today.year, today.month, today.day),
4222
+ # "count": 1,
4223
+ # }
4224
+ # ]
4225
+
4226
+ # # Work on a shallow copy so we don't mutate original in-place
4227
+ # result: List[Dict[str, object]] = list(existing)
4228
+
4229
+ # last_entry = result[-1] if result else None
4230
+ # if not last_entry or "date" not in last_entry:
4231
+ # # If structure is unexpected, re-initialize safely
4232
+ # return [
4233
+ # {
4234
+ # "date": datetime(today.year, today.month, today.day),
4235
+ # "count": 1,
4236
+ # }
4237
+ # ]
4238
+
4239
+ # last_date = _to_date_only(last_entry["date"])
4240
+
4241
+ # # If somehow the last stored date is in the future, do nothing to avoid corrupting history
4242
+ # if last_date > today:
4243
+ # return result
4244
+
4245
+ # # Case B: today's date already present as the last entry → unchanged
4246
+ # if last_date == today:
4247
+ # return result
4248
+
4249
+ # # Case C: there is a gap, fill missing days with count=0 and append today with count=1
4250
+ # cursor = last_date + timedelta(days=1)
4251
+ # while cursor < today:
4252
+ # result.append(
4253
+ # {
4254
+ # "date": datetime(cursor.year, cursor.month, cursor.day),
4255
+ # "count": 0,
4256
+ # }
4257
+ # )
4258
+ # cursor += timedelta(days=1)
4259
+
4260
+ # # Finally add today's presence indicator
4261
+ # result.append(
4262
+ # {
4263
+ # "date": datetime(today.year, today.month, today.day),
4264
+ # "count": 1,
4265
+ # }
4266
+ # )
4267
+
4268
+ # # Sort by date ascending (older dates first) to guarantee stable ordering:
4269
+ # # [oldest, ..., newest]
4270
+ # try:
4271
+ # result.sort(key=lambda entry: _to_date_only(entry.get("date")))
4272
+ # except Exception:
4273
+ # # If anything goes wrong during sort, fall back to current ordering
4274
+ # pass
4275
+
4276
+ # # Enforce 32-entry limit (keep the most recent 32 days)
4277
+ # if len(result) > 32:
4278
+ # result = result[-32:]
4279
+
4280
+ # return result
4281
+
4282
+ # def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
4283
+ # if not ENV_TOKEN:
4284
+ # return
4285
+ # if authorization is None or not authorization.lower().startswith("bearer "):
4286
+ # raise HTTPException(status_code=401, detail="Unauthorized")
4287
+ # token = authorization.split(" ", 1)[1]
4288
+ # if token != ENV_TOKEN:
4289
+ # raise HTTPException(status_code=403, detail="Forbidden")
4290
+
4291
+
4292
+ # class InpaintRequest(BaseModel):
4293
+ # image_id: str
4294
+ # mask_id: str
4295
+ # invert_mask: bool = True # True => selected/painted area is removed
4296
+ # passthrough: bool = False # If True, return the original image unchanged
4297
+ # user_id: Optional[str] = None
4298
+ # category_id: Optional[str] = None
4299
+
4300
+
4301
+ # class SimpleRemoveRequest(BaseModel):
4302
+ # image_id: str # Image with pink/magenta segments to remove
4303
+
4304
+
4305
+ # def _coerce_object_id(value: Optional[str]) -> ObjectId:
4306
+ # if value is None:
4307
+ # return ObjectId()
4308
+ # value_str = str(value).strip()
4309
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", value_str):
4310
+ # return ObjectId(value_str)
4311
+ # if value_str.isdigit():
4312
+ # hex_str = format(int(value_str), "x")
4313
+ # if len(hex_str) > 24:
4314
+ # hex_str = hex_str[-24:]
4315
+ # hex_str = hex_str.rjust(24, "0")
4316
+ # return ObjectId(hex_str)
4317
+ # return ObjectId()
4318
+
4319
+
4320
+ # def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
4321
+ # raw = category_id or DEFAULT_CATEGORY_ID
4322
+ # raw_str = str(raw).strip()
4323
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", raw_str):
4324
+ # return ObjectId(raw_str)
4325
+ # return _coerce_object_id(raw_str)
4326
+
4327
+
4328
+ # def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
4329
+ # """Log to admin media_clicks collection only if user_id is provided."""
4330
+ # if admin_media_clicks is None:
4331
+ # return
4332
+ # # Only log if user_id is provided (not None/empty)
4333
+ # if not user_id or not user_id.strip():
4334
+ # return
4335
+ # try:
4336
+ # user_obj = _coerce_object_id(user_id)
4337
+ # category_obj = _coerce_category_id(category_id)
4338
+ # now = datetime.utcnow()
4339
+ # today = now.date()
4340
+
4341
+ # doc = admin_media_clicks.find_one({"userId": user_obj})
4342
+ # if doc:
4343
+ # existing_daily = doc.get("ai_edit_daily_count")
4344
+ # updated_daily = _build_ai_edit_daily_count(existing_daily, today)
4345
+ # categories = doc.get("categories") or []
4346
+ # if any(cat.get("categoryId") == category_obj for cat in categories):
4347
+ # # Category exists: increment click_count and ai_edit_complete, update dates
4348
+ # admin_media_clicks.update_one(
4349
+ # {"_id": doc["_id"], "categories.categoryId": category_obj},
4350
+ # {
4351
+ # "$inc": {
4352
+ # "categories.$.click_count": 1,
4353
+ # "ai_edit_complete": 1, # $inc handles missing fields (backward compatible)
4354
+ # },
4355
+ # "$set": {
4356
+ # "categories.$.lastClickedAt": now,
4357
+ # "updatedAt": now,
4358
+ # "ai_edit_last_date": now,
4359
+ # "ai_edit_daily_count": updated_daily,
4360
+ # },
4361
+ # },
4362
+ # )
4363
+ # else:
4364
+ # # New category to existing document: push category, increment ai_edit_complete
4365
+ # admin_media_clicks.update_one(
4366
+ # {"_id": doc["_id"]},
4367
+ # {
4368
+ # "$push": {
4369
+ # "categories": {
4370
+ # "categoryId": category_obj,
4371
+ # "click_count": 1,
4372
+ # "lastClickedAt": now,
4373
+ # }
4374
+ # },
4375
+ # "$inc": {"ai_edit_complete": 1}, # $inc handles missing fields
4376
+ # "$set": {
4377
+ # "updatedAt": now,
4378
+ # "ai_edit_last_date": now,
4379
+ # "ai_edit_daily_count": updated_daily,
4380
+ # },
4381
+ # },
4382
+ # )
4383
+ # else:
4384
+ # # New user: create document with default ai_edit_complete=0, then increment to 1
4385
+ # daily_for_new = _build_ai_edit_daily_count(None, today)
4386
+ # admin_media_clicks.update_one(
4387
+ # {"userId": user_obj},
4388
+ # {
4389
+ # "$setOnInsert": {
4390
+ # "userId": user_obj,
4391
+ # "categories": [
4392
+ # {
4393
+ # "categoryId": category_obj,
4394
+ # "click_count": 1,
4395
+ # "lastClickedAt": now,
4396
+ # }
4397
+ # ],
4398
+ # "createdAt": now,
4399
+ # "updatedAt": now,
4400
+ # "ai_edit_daily_count": daily_for_new,
4401
+ # },
4402
+ # "$inc": {"ai_edit_complete": 1}, # Increment to 1 on first use
4403
+ # "$set": {
4404
+ # "updatedAt": now,
4405
+ # "ai_edit_last_date": now,
4406
+ # },
4407
+ # },
4408
+ # upsert=True,
4409
+ # )
4410
+ # except Exception as err:
4411
+ # err_str = str(err)
4412
+ # if "Unauthorized" in err_str or "not authorized" in err_str.lower():
4413
+ # log.warning(
4414
+ # "Admin media click logging failed (permissions): user lacks read/write on db=%s collection=%s. "
4415
+ # "Check MongoDB user permissions.",
4416
+ # admin_media_clicks.database.name,
4417
+ # admin_media_clicks.name,
4418
+ # )
4419
+ # else:
4420
+ # log.warning("Admin media click logging failed: %s", err)
4421
+
4422
+
4423
+ # @app.get("/")
4424
+ # def root() -> Dict[str, object]:
4425
+ # return {
4426
+ # "name": "Photo Object Removal API",
4427
+ # "status": "ok",
4428
+ # "endpoints": {
4429
+ # "GET /health": "health check",
4430
+ # "POST /upload-image": "form-data: image=file",
4431
+ # "POST /upload-mask": "form-data: mask=file",
4432
+ # "POST /inpaint": "JSON: {image_id, mask_id}",
4433
+ # "POST /inpaint-multipart": "form-data: image=file, mask=file",
4434
+ # "POST /remove-pink": "form-data: image=file (auto-detects pink segments and removes them)",
4435
+ # "GET /download/{filename}": "download result image",
4436
+ # "GET /result/{filename}": "view result image in browser",
4437
+ # "GET /logs": "recent uploads/results",
4438
+ # },
4439
+ # "auth": "set API_TOKEN env var to require Authorization: Bearer <token> (except /health)",
4440
+ # }
4441
+
4442
+
4443
+ # @app.get("/health")
4444
+ # def health() -> Dict[str, str]:
4445
+ # return {"status": "healthy"}
4446
+
4447
+
4448
+ # @app.get("/logging-status")
4449
+ # def logging_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
4450
+ # """Helper endpoint to verify admin media logging wiring (no secrets exposed)."""
4451
+ # return _admin_logging_status()
4452
+
4453
+
4454
+ # @app.post("/upload-image")
4455
+ # def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
4456
+ # ext = os.path.splitext(image.filename)[1] or ".png"
4457
+ # file_id = str(uuid.uuid4())
4458
+ # stored_name = f"{file_id}{ext}"
4459
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
4460
+ # with open(stored_path, "wb") as f:
4461
+ # shutil.copyfileobj(image.file, f)
4462
+ # file_store[file_id] = {
4463
+ # "type": "image",
4464
+ # "filename": image.filename,
4465
+ # "stored_name": stored_name,
4466
+ # "path": stored_path,
4467
+ # "timestamp": datetime.utcnow().isoformat(),
4468
+ # }
4469
+ # logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
4470
+ # return {"id": file_id, "filename": image.filename}
4471
+
4472
+
4473
+ # @app.post("/upload-mask")
4474
+ # def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
4475
+ # ext = os.path.splitext(mask.filename)[1] or ".png"
4476
+ # file_id = str(uuid.uuid4())
4477
+ # stored_name = f"{file_id}{ext}"
4478
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
4479
+ # with open(stored_path, "wb") as f:
4480
+ # shutil.copyfileobj(mask.file, f)
4481
+ # file_store[file_id] = {
4482
+ # "type": "mask",
4483
+ # "filename": mask.filename,
4484
+ # "stored_name": stored_name,
4485
+ # "path": stored_path,
4486
+ # "timestamp": datetime.utcnow().isoformat(),
4487
+ # }
4488
+ # logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
4489
+ # return {"id": file_id, "filename": mask.filename}
4490
+
4491
+
4492
+ # def _load_rgba_image(path: str) -> Image.Image:
4493
+ # img = Image.open(path)
4494
+ # return img.convert("RGBA")
4495
+
4496
+
4497
  # def _compress_image(image_path: str, output_path: str, quality: int = 85) -> None:
4498
  # """
4499
  # Compress an image to reduce file size.