66
77import asyncio
88import base64
9+ import io
910import math
1011import os
1112
@@ -66,6 +67,20 @@ def _detect_mime_type(data: bytes) -> str:
6667 return "image/png"
6768
6869
70+ def _build_visible_mask_reference (mask_bytes : bytes ) -> bytes :
71+ """Render an alpha edit mask as visible black/white guidance for Gemini."""
72+ from PIL import Image
73+
74+ with Image .open (io .BytesIO (mask_bytes )) as mask_img :
75+ alpha = mask_img .convert ("RGBA" ).split ()[- 1 ]
76+ visible = alpha .point (lambda value : 255 if value < 128 else 0 )
77+ rgb = Image .new ("RGB" , visible .size , (0 , 0 , 0 ))
78+ rgb .paste ((255 , 255 , 255 ), mask = visible )
79+ buf = io .BytesIO ()
80+ rgb .save (buf , format = "PNG" )
81+ return buf .getvalue ()
82+
83+
6984class GeminiImageProvider :
7085 """Image generation via Google Gemini API.
7186
@@ -279,6 +294,7 @@ async def inpaint_with_mask(
279294 image_bytes = image_fh .read ()
280295 with open (mask_path , "rb" ) as mask_fh :
281296 mask_bytes = mask_fh .read ()
297+ visible_mask_bytes = _build_visible_mask_reference (mask_bytes )
282298
283299 if size and "x" in size :
284300 try :
@@ -295,11 +311,15 @@ async def inpaint_with_mask(
295311 aspect_ratio = _dims_to_aspect_ratio (width , height )
296312 full_prompt = (
297313 f"{ prompt } \n \n "
298- "Use the first image as the source crop. Use the second image as an "
299- "RGBA edit mask: transparent mask pixels mark the edit region, and "
300- "opaque mask pixels mark source context that should stay visually "
301- "preserved. Repaint only the transparent mask pixels. Do not create "
302- "a new scene outside the masked replacement area."
314+ "Use the first image as the source crop. Use the second image as a "
315+ "visible binary edit mask rendered from the original RGBA mask: "
316+ "white mask pixels (original transparent mask pixels) mark the edit "
317+ "region, and black mask pixels (original opaque mask pixels) mark "
318+ "source context that should stay visually preserved. Repaint only "
319+ "the white edit region. Do not create a new scene outside the masked "
320+ "replacement area. The mask image is not part of the output: do not "
321+ "draw the mask, do not copy its white or black shapes, and do not "
322+ "leave mask-colored background behind."
303323 )
304324 if tradition and tradition != "default" :
305325 full_prompt += (
@@ -314,8 +334,8 @@ async def inpaint_with_mask(
314334 mime_type = _detect_mime_type (image_bytes ),
315335 ),
316336 types .Part .from_bytes (
317- data = mask_bytes ,
318- mime_type = _detect_mime_type ( mask_bytes ) ,
337+ data = visible_mask_bytes ,
338+ mime_type = "image/png" ,
319339 ),
320340 full_prompt ,
321341 ]
0 commit comments