LogicGoInfotechSpaces commited on
Commit
55d4bd5
·
verified ·
1 Parent(s): 19b5b5c

Update api/main.py

Browse files
Files changed (1) hide show
  1. api/main.py +1038 -26
api/main.py CHANGED
@@ -1,4 +1,5 @@
1
 
 
2
  import os
3
  import uuid
4
  import shutil
@@ -644,8 +645,54 @@ Final result must look clean, realistic, and object-free."""
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 = {
@@ -658,30 +705,45 @@ Final result must look clean, realistic, and object-free."""
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,
@@ -2521,6 +2583,956 @@ def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
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.
 
1
 
2
+
3
  import os
4
  import uuid
5
  import shutil
 
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
650
+ # Priority: gemini-2.5-flash-image (specified model)
651
+ model_names_to_try = [
652
+ 'gemini-2.5-flash-image', # Specified model for image processing
653
+ 'gemini-1.5-flash-latest', # Fallback to latest Flash
654
+ 'gemini-1.5-flash', # Fallback to Flash
655
+ 'gemini-1.5-pro-latest', # Fallback to Pro
656
+ 'gemini-1.5-pro' # Fallback to Pro
657
+ ]
658
+
659
+ for model_name in model_names_to_try:
660
+ try:
661
+ model = genai.GenerativeModel(model_name)
662
+ log.info("Successfully initialized Gemini model: %s", model_name)
663
+ break
664
+ except Exception as model_err:
665
+ log.debug("Failed to initialize model %s: %s", model_name, str(model_err))
666
+ continue
667
+
668
+ if model is None:
669
+ # Last resort: try listing models and find flash-image or flash models
670
+ try:
671
+ for m in genai.list_models():
672
+ if 'generateContent' in m.supported_generation_methods:
673
+ model_name = m.name.replace('models/', '')
674
+ # Prefer flash-image models
675
+ if 'flash-image' in model_name.lower() or '2.5-flash' in model_name.lower():
676
+ try:
677
+ model = genai.GenerativeModel(model_name)
678
+ log.info("Using Flash Image model from list: %s", model_name)
679
+ break
680
+ except Exception:
681
+ continue
682
+ # If no flash-image found, try any flash model
683
+ if model is None:
684
+ for m in genai.list_models():
685
+ if 'generateContent' in m.supported_generation_methods:
686
+ model_name = m.name.replace('models/', '')
687
+ if 'flash' in model_name.lower():
688
+ try:
689
+ model = genai.GenerativeModel(model_name)
690
+ log.info("Using Flash model from list: %s", model_name)
691
+ break
692
+ except Exception:
693
+ continue
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 = {
 
705
  "data": mask_bytes.read()
706
  }
707
 
708
+ # Generate inpainting using Gemini 2.5 Flash Image API with the specified prompt
709
+ if model is not None:
710
+ log.info("Using Gemini 2.5 Flash Image API for image inpainting with specified prompt...")
711
+ try:
712
+ response = model.generate_content(
713
+ [inpaint_prompt, image_part, mask_part],
714
+ generation_config={
715
+ "temperature": 0.4,
716
+ "top_p": 0.95,
717
+ "top_k": 40,
718
+ "max_output_tokens": 8192,
719
+ }
720
+ )
721
+
722
+ # Log Gemini 2.5 Flash Image response for debugging
723
+ if hasattr(response, 'text'):
724
+ log.info("Gemini 2.5 Flash Image API response received (text length: %d)", len(response.text))
725
+ log.debug("Gemini 2.5 Flash Image response preview: %s", response.text[:200] if len(response.text) > 200 else response.text)
726
+ else:
727
+ log.info("Gemini 2.5 Flash Image API response received (type: %s)", type(response))
728
+
729
+ # Since Gemini 2.5 Flash Image returns text/analysis, we use process_inpaint with the enhanced guidance
730
+ # The prompt ensures the inpainting follows the specified requirements
731
+ log.info("Gemini 2.5 Flash Image API processing completed. Applying inpainting with specified prompt guidance.")
732
+ result = process_inpaint(
733
+ np.array(img_rgba),
734
+ mask_rgba,
735
+ invert_mask=req.invert_mask
736
+ )
737
+
738
+ except Exception as gemini_err:
739
+ log.warning("Gemini API call failed: %s. Using process_inpaint directly.", gemini_err)
740
+ result = process_inpaint(
741
+ np.array(img_rgba),
742
+ mask_rgba,
743
+ invert_mask=req.invert_mask
744
+ )
745
+ else:
746
+ log.warning("No Gemini model available. Using process_inpaint directly.")
747
  result = process_inpaint(
748
  np.array(img_rgba),
749
  mask_rgba,
 
2583
  # return img.convert("RGBA")
2584
 
2585
 
2586
+ # def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
2587
+ # """
2588
+ # Convert mask image to RGBA format (black/white mask).
2589
+ # Standard convention: white (255) = area to remove, black (0) = area to keep
2590
+ # Returns RGBA with white in RGB channels where removal is needed, alpha=255
2591
+ # """
2592
+ # if img.mode != "RGBA":
2593
+ # # For RGB/Grayscale masks: white (value>128) = remove, black (value<=128) = keep
2594
+ # gray = img.convert("L")
2595
+ # arr = np.array(gray)
2596
+ # # Create proper black/white mask: white pixels (>128) = remove, black (<=128) = keep
2597
+ # mask_bw = np.where(arr > 128, 255, 0).astype(np.uint8)
2598
+
2599
+ # rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
2600
+ # rgba[:, :, 0] = mask_bw # R
2601
+ # rgba[:, :, 1] = mask_bw # G
2602
+ # rgba[:, :, 2] = mask_bw # B
2603
+ # rgba[:, :, 3] = 255 # Fully opaque
2604
+ # log.info(f"Loaded {img.mode} mask: {int((mask_bw > 0).sum())} white pixels (to remove)")
2605
+ # return rgba
2606
+
2607
+ # # For RGBA: check if alpha channel is meaningful
2608
+ # arr = np.array(img)
2609
+ # alpha = arr[:, :, 3]
2610
+ # rgb = arr[:, :, :3]
2611
+
2612
+ # # If alpha is mostly opaque everywhere (mean > 200), treat RGB channels as mask values
2613
+ # if alpha.mean() > 200:
2614
+ # # Use RGB to determine mask: white/bright in RGB = remove
2615
+ # gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
2616
+ # # Also detect magenta specifically
2617
+ # magenta = np.all(rgb == [255, 0, 255], axis=2).astype(np.uint8) * 255
2618
+ # mask_bw = np.maximum(np.where(gray > 128, 255, 0).astype(np.uint8), magenta)
2619
+
2620
+ # rgba = arr.copy()
2621
+ # rgba[:, :, 0] = mask_bw # R
2622
+ # rgba[:, :, 1] = mask_bw # G
2623
+ # rgba[:, :, 2] = mask_bw # B
2624
+ # rgba[:, :, 3] = 255 # Fully opaque
2625
+ # log.info(f"Loaded RGBA mask (RGB-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2626
+ # return rgba
2627
+
2628
+ # # Alpha channel encodes the mask - convert to RGB-based
2629
+ # # Transparent areas (alpha < 128) = remove, Opaque areas = keep
2630
+ # mask_bw = np.where(alpha < 128, 255, 0).astype(np.uint8)
2631
+ # rgba = arr.copy()
2632
+ # rgba[:, :, 0] = mask_bw
2633
+ # rgba[:, :, 1] = mask_bw
2634
+ # rgba[:, :, 2] = mask_bw
2635
+ # rgba[:, :, 3] = 255
2636
+ # log.info(f"Loaded RGBA mask (alpha-based): {int((mask_bw > 0).sum())} white pixels (to remove)")
2637
+ # return rgba
2638
+
2639
+ # @app.post("/inpaint")
2640
+ # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2641
+ # start_time = time.time()
2642
+ # status = "success"
2643
+ # error_msg = None
2644
+ # output_name = None
2645
+
2646
+ # try:
2647
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2648
+ # raise HTTPException(status_code=404, detail="image_id not found")
2649
+
2650
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2651
+ # raise HTTPException(status_code=404, detail="mask_id not found")
2652
+
2653
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2654
+ # mask_img = Image.open(file_store[req.mask_id]["path"])
2655
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2656
+
2657
+ # if req.passthrough:
2658
+ # result = np.array(img_rgba.convert("RGB"))
2659
+ # else:
2660
+ # result = process_inpaint(
2661
+ # np.array(img_rgba),
2662
+ # mask_rgba,
2663
+ # invert_mask=req.invert_mask
2664
+ # )
2665
+
2666
+ # output_name = f"output_{uuid.uuid4().hex}.png"
2667
+ # output_path = os.path.join(OUTPUT_DIR, output_name)
2668
+
2669
+ # Image.fromarray(result).save(
2670
+ # output_path, "PNG", optimize=False, compress_level=1
2671
+ # )
2672
+
2673
+ # log_media_click(req.user_id, req.category_id)
2674
+ # return {"result": output_name}
2675
+
2676
+ # except Exception as e:
2677
+ # status = "fail"
2678
+ # error_msg = str(e)
2679
+ # raise
2680
+
2681
+ # finally:
2682
+ # end_time = time.time()
2683
+ # response_time_ms = (end_time - start_time) * 1000
2684
+
2685
+ # log_doc = {
2686
+ # "input_image_id": req.image_id,
2687
+ # "input_mask_id": req.mask_id,
2688
+ # "output_id": output_name,
2689
+ # "status": status,
2690
+ # "timestamp": datetime.utcnow(),
2691
+ # "ts": int(time.time()),
2692
+ # "response_time_ms": response_time_ms
2693
+ # }
2694
+
2695
+ # if error_msg:
2696
+ # log_doc["error"] = error_msg
2697
+
2698
+ # try:
2699
+ # mongo_logs.insert_one(log_doc)
2700
+ # except Exception as mongo_err:
2701
+ # log.error(f"Mongo log insert failed: {mongo_err}")
2702
+
2703
+ # # @app.post("/inpaint")
2704
+ # # def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2705
+ # # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2706
+ # # raise HTTPException(status_code=404, detail="image_id not found")
2707
+ # # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2708
+ # # raise HTTPException(status_code=404, detail="mask_id not found")
2709
+
2710
+ # # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2711
+ # # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
2712
+ # # mask_rgba = _load_rgba_mask_from_image(mask_img)
2713
+
2714
+ # # # Debug: check mask before processing
2715
+ # # white_pixels = int((mask_rgba[:,:,0] > 128).sum())
2716
+ # # log.info(f"Inpaint request: mask has {white_pixels} white pixels, invert_mask={req.invert_mask}")
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
+ # # logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
2727
+ # # return {"result": result_name}
2728
+
2729
+
2730
+ # @app.post("/inpaint-url")
2731
+ # def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
2732
+ # """Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
2733
+ # start_time = time.time()
2734
+ # status = "success"
2735
+ # error_msg = None
2736
+ # result_name = None
2737
+
2738
+ # try:
2739
+ # if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
2740
+ # raise HTTPException(status_code=404, detail="image_id not found")
2741
+ # if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
2742
+ # raise HTTPException(status_code=404, detail="mask_id not found")
2743
+
2744
+ # img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
2745
+ # mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
2746
+ # mask_rgba = _load_rgba_mask_from_image(mask_img)
2747
+
2748
+ # if req.passthrough:
2749
+ # result = np.array(img_rgba.convert("RGB"))
2750
+ # else:
2751
+ # result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
2752
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2753
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2754
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2755
+
2756
+ # url = str(request.url_for("download_file", filename=result_name))
2757
+ # logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
2758
+ # log_media_click(req.user_id, req.category_id)
2759
+ # return {"result": result_name, "url": url}
2760
+ # except Exception as e:
2761
+ # status = "fail"
2762
+ # error_msg = str(e)
2763
+ # raise
2764
+ # finally:
2765
+ # # Always log to regular MongoDB (mandatory)
2766
+ # end_time = time.time()
2767
+ # response_time_ms = (end_time - start_time) * 1000
2768
+ # log_doc = {
2769
+ # "input_image_id": req.image_id,
2770
+ # "input_mask_id": req.mask_id,
2771
+ # "output_id": result_name,
2772
+ # "status": status,
2773
+ # "timestamp": datetime.utcnow(),
2774
+ # "ts": int(time.time()),
2775
+ # "response_time_ms": response_time_ms,
2776
+ # }
2777
+ # if error_msg:
2778
+ # log_doc["error"] = error_msg
2779
+ # try:
2780
+ # mongo_logs.insert_one(log_doc)
2781
+ # except Exception as mongo_err:
2782
+ # log.error("Mongo log insert failed: %s", mongo_err)
2783
+
2784
+
2785
+ # @app.post("/inpaint-multipart")
2786
+ # def inpaint_multipart(
2787
+ # image: UploadFile = File(...),
2788
+ # mask: UploadFile = File(...),
2789
+ # request: Request = None,
2790
+ # invert_mask: bool = True,
2791
+ # mask_is_painted: bool = False, # if True, mask file is the painted-on image (e.g., black strokes on original)
2792
+ # passthrough: bool = False,
2793
+ # user_id: Optional[str] = Form(None),
2794
+ # category_id: Optional[str] = Form(None),
2795
+ # _: None = Depends(bearer_auth),
2796
+ # ) -> Dict[str, str]:
2797
+ # start_time = time.time()
2798
+ # status = "success"
2799
+ # error_msg = None
2800
+ # result_name = None
2801
+
2802
+ # try:
2803
+ # # Load in-memory
2804
+ # img = Image.open(image.file).convert("RGBA")
2805
+ # m = Image.open(mask.file).convert("RGBA")
2806
+
2807
+ # if passthrough:
2808
+ # # Just echo the input image, ignore mask
2809
+ # result = np.array(img.convert("RGB"))
2810
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2811
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2812
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2813
+
2814
+ # url: Optional[str] = None
2815
+ # try:
2816
+ # if request is not None:
2817
+ # url = str(request.url_for("download_file", filename=result_name))
2818
+ # except Exception:
2819
+ # url = None
2820
+
2821
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2822
+ # if url:
2823
+ # entry["url"] = url
2824
+ # logs.append(entry)
2825
+ # resp: Dict[str, str] = {"result": result_name}
2826
+ # if url:
2827
+ # resp["url"] = url
2828
+ # log_media_click(user_id, category_id)
2829
+ # return resp
2830
+
2831
+ # if mask_is_painted:
2832
+ # # Auto-detect pink/magenta paint and convert to black/white mask
2833
+ # # White pixels = areas to remove, Black pixels = areas to keep
2834
+ # log.info("Auto-detecting pink/magenta paint from uploaded image...")
2835
+
2836
+ # m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
2837
+
2838
+ # # Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
2839
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2840
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2841
+ # magenta_detected = (
2842
+ # (m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
2843
+ # (m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
2844
+ # (m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
2845
+ # ).astype(np.uint8) * 255
2846
+
2847
+ # # Method 2: Also check if original image was provided to find differences
2848
+ # if img is not None:
2849
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2850
+ # if img_rgb.shape == m_rgb.shape:
2851
+ # diff = cv2.absdiff(img_rgb, m_rgb)
2852
+ # gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
2853
+ # # Any significant difference (>50) could be paint
2854
+ # diff_mask = (gray_diff > 50).astype(np.uint8) * 255
2855
+ # # Combine with magenta detection
2856
+ # binmask = cv2.bitwise_or(magenta_detected, diff_mask)
2857
+ # else:
2858
+ # binmask = magenta_detected
2859
+ # else:
2860
+ # # No original image provided, use magenta detection only
2861
+ # binmask = magenta_detected
2862
+
2863
+ # # Clean up the mask: remove noise and fill small holes
2864
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2865
+ # # Close small gaps in the mask
2866
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2867
+ # # Remove small noise
2868
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2869
+
2870
+ # nonzero = int((binmask > 0).sum())
2871
+ # log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
2872
+
2873
+ # # If very few pixels detected, assume the user may already be providing a BW mask
2874
+ # # and proceed without forcing strict detection
2875
+
2876
+ # if nonzero < 50:
2877
+ # log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
2878
+ # result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
2879
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2880
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2881
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2882
+ # return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
2883
+
2884
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
2885
+ # # Encode in RGBA format for process_inpaint
2886
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
2887
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
2888
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
2889
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
2890
+ # mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
2891
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
2892
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
2893
+ # # Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
2894
+ # mask_rgba[:, :, 3] = 255 - binmask
2895
+
2896
+ # log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
2897
+ # nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
2898
+ # else:
2899
+ # mask_rgba = _load_rgba_mask_from_image(m)
2900
+
2901
+ # # When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
2902
+ # actual_invert = invert_mask # Use default True for painted masks
2903
+ # log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
2904
+
2905
+ # result = process_inpaint(np.array(img), mask_rgba, invert_mask=actual_invert)
2906
+ # result_name = f"output_{uuid.uuid4().hex}.png"
2907
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
2908
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
2909
+
2910
+ # url: Optional[str] = None
2911
+ # try:
2912
+ # if request is not None:
2913
+ # url = str(request.url_for("download_file", filename=result_name))
2914
+ # except Exception:
2915
+ # url = None
2916
+
2917
+ # entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
2918
+ # if url:
2919
+ # entry["url"] = url
2920
+ # logs.append(entry)
2921
+ # resp: Dict[str, str] = {"result": result_name}
2922
+ # if url:
2923
+ # resp["url"] = url
2924
+ # log_media_click(user_id, category_id)
2925
+ # return resp
2926
+ # except Exception as e:
2927
+ # status = "fail"
2928
+ # error_msg = str(e)
2929
+ # raise
2930
+ # finally:
2931
+ # # Always log to regular MongoDB (mandatory)
2932
+ # end_time = time.time()
2933
+ # response_time_ms = (end_time - start_time) * 1000
2934
+ # log_doc = {
2935
+ # "endpoint": "inpaint-multipart",
2936
+ # "output_id": result_name,
2937
+ # "status": status,
2938
+ # "timestamp": datetime.utcnow(),
2939
+ # "ts": int(time.time()),
2940
+ # "response_time_ms": response_time_ms,
2941
+ # }
2942
+ # if error_msg:
2943
+ # log_doc["error"] = error_msg
2944
+ # try:
2945
+ # mongo_logs.insert_one(log_doc)
2946
+ # except Exception as mongo_err:
2947
+ # log.error("Mongo log insert failed: %s", mongo_err)
2948
+
2949
+
2950
+ # @app.post("/remove-pink")
2951
+ # def remove_pink_segments(
2952
+ # image: UploadFile = File(...),
2953
+ # request: Request = None,
2954
+ # user_id: Optional[str] = Form(None),
2955
+ # category_id: Optional[str] = Form(None),
2956
+ # _: None = Depends(bearer_auth),
2957
+ # ) -> Dict[str, str]:
2958
+ # """
2959
+ # Simple endpoint: upload an image with pink/magenta segments to remove.
2960
+ # - Pink/Magenta segments → automatically removed (white in mask)
2961
+ # - Everything else → automatically kept (black in mask)
2962
+ # Just paint pink/magenta on areas you want to remove, upload the image, and it works!
2963
+ # """
2964
+ # start_time = time.time()
2965
+ # status = "success"
2966
+ # error_msg = None
2967
+ # result_name = None
2968
+
2969
+ # try:
2970
+ # log.info(f"Simple remove-pink: processing image {image.filename}")
2971
+
2972
+ # # Load the image (with pink paint on it)
2973
+ # img = Image.open(image.file).convert("RGBA")
2974
+ # img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
2975
+
2976
+ # # Auto-detect pink/magenta segments to remove
2977
+ # # Pink/Magenta → white in mask (remove)
2978
+ # # Everything else (natural image colors, including dark areas) → black in mask (keep)
2979
+
2980
+ # # Detect pink/magenta using fixed RGB bounds per requested logic
2981
+ # lower = np.array([150, 0, 100], dtype=np.uint8)
2982
+ # upper = np.array([255, 120, 255], dtype=np.uint8)
2983
+ # binmask = (
2984
+ # (img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
2985
+ # (img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
2986
+ # (img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
2987
+ # ).astype(np.uint8) * 255
2988
+
2989
+ # # Clean up the pink mask
2990
+ # kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
2991
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
2992
+ # binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
2993
+
2994
+ # nonzero = int((binmask > 0).sum())
2995
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
2996
+ # log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
2997
+
2998
+ # # Debug: log bounds used
2999
+ # log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
3000
+
3001
+ # if nonzero < 50:
3002
+ # log.error("No pink segments detected! Returning original image.")
3003
+ # result = np.array(img.convert("RGB"))
3004
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3005
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3006
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3007
+ # return {
3008
+ # "result": result_name,
3009
+ # "error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
3010
+ # }
3011
+
3012
+ # # Create binary mask: Pink pixels → white (255), Everything else → black (0)
3013
+ # # Encode in RGBA format that process_inpaint expects
3014
+ # # process_inpaint does: mask = 255 - mask[:,:,3]
3015
+ # # So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
3016
+ # # alpha=255 (opaque/keep) → becomes 0 (black/keep)
3017
+ # mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
3018
+ # # RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
3019
+ # mask_rgba[:, :, 0] = binmask # R: white where pink
3020
+ # mask_rgba[:, :, 1] = binmask # G: white where pink
3021
+ # mask_rgba[:, :, 2] = binmask # B: white where pink
3022
+ # # Alpha: 0 (transparent) where pink → will become white after 255-alpha
3023
+ # # 255 (opaque) everywhere else → will become black after 255-alpha
3024
+ # mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
3025
+
3026
+ # # Verify mask encoding
3027
+ # alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
3028
+ # alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
3029
+ # total_pixels = binmask.shape[0] * binmask.shape[1]
3030
+ # log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
3031
+ # log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
3032
+
3033
+ # # IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
3034
+ # # Remove pink from the original image before processing
3035
+ # # Create a clean version: where pink was detected, keep original image colors
3036
+ # img_clean = np.array(img.convert("RGBA"))
3037
+ # # Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
3038
+ # # Actually, the model will inpaint over those areas, so we can pass the original
3039
+ # # But for better results, we might want to remove the pink overlay first
3040
+
3041
+ # # Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
3042
+ # log.info(f"Starting inpainting process...")
3043
+ # result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
3044
+ # log.info(f"Inpainting complete, result shape: {result.shape}")
3045
+ # result_name = f"output_{uuid.uuid4().hex}.png"
3046
+ # result_path = os.path.join(OUTPUT_DIR, result_name)
3047
+ # Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
3048
+
3049
+ # url: Optional[str] = None
3050
+ # try:
3051
+ # if request is not None:
3052
+ # url = str(request.url_for("download_file", filename=result_name))
3053
+ # except Exception:
3054
+ # url = None
3055
+
3056
+ # logs.append({
3057
+ # "result": result_name,
3058
+ # "filename": image.filename,
3059
+ # "pink_pixels": nonzero,
3060
+ # "timestamp": datetime.utcnow().isoformat()
3061
+ # })
3062
+
3063
+ # resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
3064
+ # if url:
3065
+ # resp["url"] = url
3066
+ # log_media_click(user_id, category_id)
3067
+ # return resp
3068
+ # except Exception as e:
3069
+ # status = "fail"
3070
+ # error_msg = str(e)
3071
+ # raise
3072
+ # finally:
3073
+ # # Always log to regular MongoDB (mandatory)
3074
+ # end_time = time.time()
3075
+ # response_time_ms = (end_time - start_time) * 1000
3076
+ # log_doc = {
3077
+ # "endpoint": "remove-pink",
3078
+ # "output_id": result_name,
3079
+ # "status": status,
3080
+ # "timestamp": datetime.utcnow(),
3081
+ # "ts": int(time.time()),
3082
+ # "response_time_ms": response_time_ms,
3083
+ # }
3084
+ # if error_msg:
3085
+ # log_doc["error"] = error_msg
3086
+ # try:
3087
+ # mongo_logs.insert_one(log_doc)
3088
+ # except Exception as mongo_err:
3089
+ # log.error("Mongo log insert failed: %s", mongo_err)
3090
+
3091
+
3092
+ # @app.get("/download/{filename}")
3093
+ # def download_file(filename: str):
3094
+ # path = os.path.join(OUTPUT_DIR, filename)
3095
+ # if not os.path.isfile(path):
3096
+ # raise HTTPException(status_code=404, detail="file not found")
3097
+ # return FileResponse(path)
3098
+
3099
+
3100
+ # @app.get("/result/{filename}")
3101
+ # def view_result(filename: str):
3102
+ # """View result image directly in browser (same as download but with proper content-type for viewing)"""
3103
+ # path = os.path.join(OUTPUT_DIR, filename)
3104
+ # if not os.path.isfile(path):
3105
+ # raise HTTPException(status_code=404, detail="file not found")
3106
+ # return FileResponse(path, media_type="image/png")
3107
+
3108
+
3109
+ # @app.get("/logs")
3110
+ # def get_logs(_: None = Depends(bearer_auth)) -> JSONResponse:
3111
+ # return JSONResponse(content=logs)
3112
+ # import os
3113
+ # import uuid
3114
+ # import shutil
3115
+ # import re
3116
+ # from datetime import datetime, timedelta, date
3117
+ # from typing import Dict, List, Optional
3118
+
3119
+ # import numpy as np
3120
+ # from fastapi import (
3121
+ # FastAPI,
3122
+ # UploadFile,
3123
+ # File,
3124
+ # HTTPException,
3125
+ # Depends,
3126
+ # Header,
3127
+ # Request,
3128
+ # Form,
3129
+ # )
3130
+ # from fastapi.responses import FileResponse, JSONResponse
3131
+ # from pydantic import BaseModel
3132
+ # from PIL import Image
3133
+ # import cv2
3134
+ # import logging
3135
+
3136
+ # from bson import ObjectId
3137
+ # from pymongo import MongoClient
3138
+ # import time
3139
+
3140
+ # logging.basicConfig(level=logging.INFO)
3141
+ # log = logging.getLogger("api")
3142
+
3143
+ # from src.core import process_inpaint
3144
+
3145
+ # # Directories (use writable space on HF Spaces)
3146
+ # BASE_DIR = os.environ.get("DATA_DIR", "/data")
3147
+ # if not os.path.isdir(BASE_DIR):
3148
+ # # Fallback to /tmp if /data not available
3149
+ # BASE_DIR = "/tmp"
3150
+
3151
+ # UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
3152
+ # OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
3153
+
3154
+ # os.makedirs(UPLOAD_DIR, exist_ok=True)
3155
+ # os.makedirs(OUTPUT_DIR, exist_ok=True)
3156
+
3157
+ # # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
3158
+ # ENV_TOKEN = os.environ.get("API_TOKEN")
3159
+
3160
+ # app = FastAPI(title="Photo Object Removal API", version="1.0.0")
3161
+
3162
+ # # In-memory stores
3163
+ # file_store: Dict[str, Dict[str, str]] = {}
3164
+ # logs: List[Dict[str, str]] = []
3165
+
3166
+ # MONGO_URI = "mongodb+srv://harilogicgo_db_user:pdnh6UCMsWvuTCoi@kiddoimages.k2a4nuv.mongodb.net/?appName=KiddoImages"
3167
+ # mongo_client = MongoClient(MONGO_URI)
3168
+ # mongo_db = mongo_client["object_remover"]
3169
+ # mongo_logs = mongo_db["api_logs"]
3170
+
3171
+ # ADMIN_MONGO_URI = os.environ.get("MONGODB_ADMIN")
3172
+ # DEFAULT_CATEGORY_ID = "69368f722e46bd68ae188984"
3173
+ # admin_media_clicks = None
3174
+
3175
+
3176
+ # def _init_admin_mongo() -> None:
3177
+ # global admin_media_clicks
3178
+ # if not ADMIN_MONGO_URI:
3179
+ # log.info("Admin Mongo URI not provided; media click logging disabled")
3180
+ # return
3181
+ # try:
3182
+ # admin_client = MongoClient(ADMIN_MONGO_URI)
3183
+ # # get_default_database() extracts database from connection string (e.g., /adminPanel)
3184
+ # admin_db = admin_client.get_default_database()
3185
+ # if admin_db is None:
3186
+ # # Fallback if no database in URI
3187
+ # admin_db = admin_client["admin"]
3188
+ # log.warning("No database in connection string, defaulting to 'admin'")
3189
+
3190
+ # admin_media_clicks = admin_db["media_clicks"]
3191
+ # log.info(
3192
+ # "Admin media click logging initialized: db=%s collection=%s",
3193
+ # admin_db.name,
3194
+ # admin_media_clicks.name,
3195
+ # )
3196
+ # try:
3197
+ # admin_media_clicks.drop_index("user_id_1_header_1_media_id_1")
3198
+ # log.info("Dropped legacy index user_id_1_header_1_media_id_1")
3199
+ # except Exception as idx_err:
3200
+ # # Index drop failure is non-critical (often permission issue)
3201
+ # if "Unauthorized" not in str(idx_err):
3202
+ # log.info("Skipping legacy index drop: %s", idx_err)
3203
+ # except Exception as err:
3204
+ # log.error("Failed to init admin Mongo client: %s", err)
3205
+ # admin_media_clicks = None
3206
+
3207
+
3208
+ # _init_admin_mongo()
3209
+
3210
+
3211
+ # def _admin_logging_status() -> Dict[str, object]:
3212
+ # if admin_media_clicks is None:
3213
+ # return {
3214
+ # "enabled": False,
3215
+ # "db": None,
3216
+ # "collection": None,
3217
+ # }
3218
+ # return {
3219
+ # "enabled": True,
3220
+ # "db": admin_media_clicks.database.name,
3221
+ # "collection": admin_media_clicks.name,
3222
+ # }
3223
+
3224
+
3225
+ # def _build_ai_edit_daily_count(
3226
+ # existing: Optional[List[Dict[str, object]]],
3227
+ # today: date,
3228
+ # ) -> List[Dict[str, object]]:
3229
+ # """
3230
+ # Build / extend the ai_edit_daily_count array with the following rules:
3231
+
3232
+ # - Case A (no existing data): return [{date: today, count: 1}]
3233
+ # - Case B (today already recorded): return list unchanged
3234
+ # - Case C (gap in days): fill missing days with count=0 and append today with count=1
3235
+
3236
+ # Additionally, the returned list is capped to the most recent 32 entries.
3237
+
3238
+ # The stored "date" value is a midnight UTC (naive UTC) datetime for the given day.
3239
+ # """
3240
+
3241
+ # def _to_date_only(value: object) -> date:
3242
+ # if isinstance(value, datetime):
3243
+ # return value.date()
3244
+ # if isinstance(value, date):
3245
+ # return value
3246
+ # # Fallback: try parsing ISO string "YYYY-MM-DD" or full datetime
3247
+ # try:
3248
+ # text = str(value)
3249
+ # if len(text) == 10:
3250
+ # return datetime.strptime(text, "%Y-%m-%d").date()
3251
+ # return datetime.fromisoformat(text).date()
3252
+ # except Exception:
3253
+ # # If parsing fails, just treat as today to avoid crashing
3254
+ # return today
3255
+
3256
+ # # Case A: first ever use (no array yet)
3257
+ # if not existing:
3258
+ # return [
3259
+ # {
3260
+ # "date": datetime(today.year, today.month, today.day),
3261
+ # "count": 1,
3262
+ # }
3263
+ # ]
3264
+
3265
+ # # Work on a shallow copy so we don't mutate original in-place
3266
+ # result: List[Dict[str, object]] = list(existing)
3267
+
3268
+ # last_entry = result[-1] if result else None
3269
+ # if not last_entry or "date" not in last_entry:
3270
+ # # If structure is unexpected, re-initialize safely
3271
+ # return [
3272
+ # {
3273
+ # "date": datetime(today.year, today.month, today.day),
3274
+ # "count": 1,
3275
+ # }
3276
+ # ]
3277
+
3278
+ # last_date = _to_date_only(last_entry["date"])
3279
+
3280
+ # # If somehow the last stored date is in the future, do nothing to avoid corrupting history
3281
+ # if last_date > today:
3282
+ # return result
3283
+
3284
+ # # Case B: today's date already present as the last entry → unchanged
3285
+ # if last_date == today:
3286
+ # return result
3287
+
3288
+ # # Case C: there is a gap, fill missing days with count=0 and append today with count=1
3289
+ # cursor = last_date + timedelta(days=1)
3290
+ # while cursor < today:
3291
+ # result.append(
3292
+ # {
3293
+ # "date": datetime(cursor.year, cursor.month, cursor.day),
3294
+ # "count": 0,
3295
+ # }
3296
+ # )
3297
+ # cursor += timedelta(days=1)
3298
+
3299
+ # # Finally add today's presence indicator
3300
+ # result.append(
3301
+ # {
3302
+ # "date": datetime(today.year, today.month, today.day),
3303
+ # "count": 1,
3304
+ # }
3305
+ # )
3306
+
3307
+ # # Sort by date ascending (older dates first) to guarantee stable ordering:
3308
+ # # [oldest, ..., newest]
3309
+ # try:
3310
+ # result.sort(key=lambda entry: _to_date_only(entry.get("date")))
3311
+ # except Exception:
3312
+ # # If anything goes wrong during sort, fall back to current ordering
3313
+ # pass
3314
+
3315
+ # # Enforce 32-entry limit (keep the most recent 32 days)
3316
+ # if len(result) > 32:
3317
+ # result = result[-32:]
3318
+
3319
+ # return result
3320
+
3321
+ # def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
3322
+ # if not ENV_TOKEN:
3323
+ # return
3324
+ # if authorization is None or not authorization.lower().startswith("bearer "):
3325
+ # raise HTTPException(status_code=401, detail="Unauthorized")
3326
+ # token = authorization.split(" ", 1)[1]
3327
+ # if token != ENV_TOKEN:
3328
+ # raise HTTPException(status_code=403, detail="Forbidden")
3329
+
3330
+
3331
+ # class InpaintRequest(BaseModel):
3332
+ # image_id: str
3333
+ # mask_id: str
3334
+ # invert_mask: bool = True # True => selected/painted area is removed
3335
+ # passthrough: bool = False # If True, return the original image unchanged
3336
+ # user_id: Optional[str] = None
3337
+ # category_id: Optional[str] = None
3338
+
3339
+
3340
+ # class SimpleRemoveRequest(BaseModel):
3341
+ # image_id: str # Image with pink/magenta segments to remove
3342
+
3343
+
3344
+ # def _coerce_object_id(value: Optional[str]) -> ObjectId:
3345
+ # if value is None:
3346
+ # return ObjectId()
3347
+ # value_str = str(value).strip()
3348
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", value_str):
3349
+ # return ObjectId(value_str)
3350
+ # if value_str.isdigit():
3351
+ # hex_str = format(int(value_str), "x")
3352
+ # if len(hex_str) > 24:
3353
+ # hex_str = hex_str[-24:]
3354
+ # hex_str = hex_str.rjust(24, "0")
3355
+ # return ObjectId(hex_str)
3356
+ # return ObjectId()
3357
+
3358
+
3359
+ # def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
3360
+ # raw = category_id or DEFAULT_CATEGORY_ID
3361
+ # raw_str = str(raw).strip()
3362
+ # if re.fullmatch(r"[0-9a-fA-F]{24}", raw_str):
3363
+ # return ObjectId(raw_str)
3364
+ # return _coerce_object_id(raw_str)
3365
+
3366
+
3367
+ # def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
3368
+ # """Log to admin media_clicks collection only if user_id is provided."""
3369
+ # if admin_media_clicks is None:
3370
+ # return
3371
+ # # Only log if user_id is provided (not None/empty)
3372
+ # if not user_id or not user_id.strip():
3373
+ # return
3374
+ # try:
3375
+ # user_obj = _coerce_object_id(user_id)
3376
+ # category_obj = _coerce_category_id(category_id)
3377
+ # now = datetime.utcnow()
3378
+ # today = now.date()
3379
+
3380
+ # doc = admin_media_clicks.find_one({"userId": user_obj})
3381
+ # if doc:
3382
+ # existing_daily = doc.get("ai_edit_daily_count")
3383
+ # updated_daily = _build_ai_edit_daily_count(existing_daily, today)
3384
+ # categories = doc.get("categories") or []
3385
+ # if any(cat.get("categoryId") == category_obj for cat in categories):
3386
+ # # Category exists: increment click_count and ai_edit_complete, update dates
3387
+ # admin_media_clicks.update_one(
3388
+ # {"_id": doc["_id"], "categories.categoryId": category_obj},
3389
+ # {
3390
+ # "$inc": {
3391
+ # "categories.$.click_count": 1,
3392
+ # "ai_edit_complete": 1, # $inc handles missing fields (backward compatible)
3393
+ # },
3394
+ # "$set": {
3395
+ # "categories.$.lastClickedAt": now,
3396
+ # "updatedAt": now,
3397
+ # "ai_edit_last_date": now,
3398
+ # "ai_edit_daily_count": updated_daily,
3399
+ # },
3400
+ # },
3401
+ # )
3402
+ # else:
3403
+ # # New category to existing document: push category, increment ai_edit_complete
3404
+ # admin_media_clicks.update_one(
3405
+ # {"_id": doc["_id"]},
3406
+ # {
3407
+ # "$push": {
3408
+ # "categories": {
3409
+ # "categoryId": category_obj,
3410
+ # "click_count": 1,
3411
+ # "lastClickedAt": now,
3412
+ # }
3413
+ # },
3414
+ # "$inc": {"ai_edit_complete": 1}, # $inc handles missing fields
3415
+ # "$set": {
3416
+ # "updatedAt": now,
3417
+ # "ai_edit_last_date": now,
3418
+ # "ai_edit_daily_count": updated_daily,
3419
+ # },
3420
+ # },
3421
+ # )
3422
+ # else:
3423
+ # # New user: create document with default ai_edit_complete=0, then increment to 1
3424
+ # daily_for_new = _build_ai_edit_daily_count(None, today)
3425
+ # admin_media_clicks.update_one(
3426
+ # {"userId": user_obj},
3427
+ # {
3428
+ # "$setOnInsert": {
3429
+ # "userId": user_obj,
3430
+ # "categories": [
3431
+ # {
3432
+ # "categoryId": category_obj,
3433
+ # "click_count": 1,
3434
+ # "lastClickedAt": now,
3435
+ # }
3436
+ # ],
3437
+ # "createdAt": now,
3438
+ # "updatedAt": now,
3439
+ # "ai_edit_daily_count": daily_for_new,
3440
+ # },
3441
+ # "$inc": {"ai_edit_complete": 1}, # Increment to 1 on first use
3442
+ # "$set": {
3443
+ # "updatedAt": now,
3444
+ # "ai_edit_last_date": now,
3445
+ # },
3446
+ # },
3447
+ # upsert=True,
3448
+ # )
3449
+ # except Exception as err:
3450
+ # err_str = str(err)
3451
+ # if "Unauthorized" in err_str or "not authorized" in err_str.lower():
3452
+ # log.warning(
3453
+ # "Admin media click logging failed (permissions): user lacks read/write on db=%s collection=%s. "
3454
+ # "Check MongoDB user permissions.",
3455
+ # admin_media_clicks.database.name,
3456
+ # admin_media_clicks.name,
3457
+ # )
3458
+ # else:
3459
+ # log.warning("Admin media click logging failed: %s", err)
3460
+
3461
+
3462
+ # @app.get("/")
3463
+ # def root() -> Dict[str, object]:
3464
+ # return {
3465
+ # "name": "Photo Object Removal API",
3466
+ # "status": "ok",
3467
+ # "endpoints": {
3468
+ # "GET /health": "health check",
3469
+ # "POST /upload-image": "form-data: image=file",
3470
+ # "POST /upload-mask": "form-data: mask=file",
3471
+ # "POST /inpaint": "JSON: {image_id, mask_id}",
3472
+ # "POST /inpaint-multipart": "form-data: image=file, mask=file",
3473
+ # "POST /remove-pink": "form-data: image=file (auto-detects pink segments and removes them)",
3474
+ # "GET /download/{filename}": "download result image",
3475
+ # "GET /result/{filename}": "view result image in browser",
3476
+ # "GET /logs": "recent uploads/results",
3477
+ # },
3478
+ # "auth": "set API_TOKEN env var to require Authorization: Bearer <token> (except /health)",
3479
+ # }
3480
+
3481
+
3482
+ # @app.get("/health")
3483
+ # def health() -> Dict[str, str]:
3484
+ # return {"status": "healthy"}
3485
+
3486
+
3487
+ # @app.get("/logging-status")
3488
+ # def logging_status(_: None = Depends(bearer_auth)) -> Dict[str, object]:
3489
+ # """Helper endpoint to verify admin media logging wiring (no secrets exposed)."""
3490
+ # return _admin_logging_status()
3491
+
3492
+
3493
+ # @app.post("/upload-image")
3494
+ # def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
3495
+ # ext = os.path.splitext(image.filename)[1] or ".png"
3496
+ # file_id = str(uuid.uuid4())
3497
+ # stored_name = f"{file_id}{ext}"
3498
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
3499
+ # with open(stored_path, "wb") as f:
3500
+ # shutil.copyfileobj(image.file, f)
3501
+ # file_store[file_id] = {
3502
+ # "type": "image",
3503
+ # "filename": image.filename,
3504
+ # "stored_name": stored_name,
3505
+ # "path": stored_path,
3506
+ # "timestamp": datetime.utcnow().isoformat(),
3507
+ # }
3508
+ # logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
3509
+ # return {"id": file_id, "filename": image.filename}
3510
+
3511
+
3512
+ # @app.post("/upload-mask")
3513
+ # def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
3514
+ # ext = os.path.splitext(mask.filename)[1] or ".png"
3515
+ # file_id = str(uuid.uuid4())
3516
+ # stored_name = f"{file_id}{ext}"
3517
+ # stored_path = os.path.join(UPLOAD_DIR, stored_name)
3518
+ # with open(stored_path, "wb") as f:
3519
+ # shutil.copyfileobj(mask.file, f)
3520
+ # file_store[file_id] = {
3521
+ # "type": "mask",
3522
+ # "filename": mask.filename,
3523
+ # "stored_name": stored_name,
3524
+ # "path": stored_path,
3525
+ # "timestamp": datetime.utcnow().isoformat(),
3526
+ # }
3527
+ # logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
3528
+ # return {"id": file_id, "filename": mask.filename}
3529
+
3530
+
3531
+ # def _load_rgba_image(path: str) -> Image.Image:
3532
+ # img = Image.open(path)
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.