@@ -74,6 +74,7 @@ def __init__(self, model_uid: str, base_url: str, auth_headers: Dict):
7474 self ._model_uid = model_uid
7575 self ._base_url = base_url
7676 self .auth_headers = auth_headers
77+ self .timeout = aiohttp .ClientTimeout (total = 1800 )
7778 self .session = aiohttp .ClientSession (
7879 connector = aiohttp .TCPConnector (force_close = True )
7980 )
@@ -356,7 +357,7 @@ async def image_to_image(
356357 else :
357358 # Single image
358359 files .append (("image" , ("image" , image , "application/octet-stream" )))
359- response = await self .session .post (url , files = files , headers = self .auth_headers )
360+ response = await self .session .post (url , data = files , headers = self .auth_headers )
360361 if response .status != 200 :
361362 raise RuntimeError (
362363 f"Failed to variants the images, detail: { await _get_error_string (response )} "
@@ -366,6 +367,157 @@ async def image_to_image(
366367 await _release_response (response )
367368 return response_data
368369
370+ async def image_edit (
371+ self ,
372+ image : Union [Union [str , bytes ], List [Union [str , bytes ]]],
373+ prompt : str ,
374+ mask : Optional [Union [str , bytes ]] = None ,
375+ n : int = 1 ,
376+ size : Optional [str ] = None ,
377+ response_format : str = "url" ,
378+ ** kwargs ,
379+ ) -> "ImageList" :
380+ """
381+ Edit image(s) by the input text and optional mask.
382+
383+ Parameters
384+ ----------
385+ image: `Union[Union[str, bytes], List[Union[str, bytes]]]`
386+ The input image(s) to edit. Can be:
387+ - Single image: file path, URL, or binary image data
388+ - Multiple images: list of file paths, URLs, or binary image data
389+ When multiple images are provided, the first image is used as the primary image
390+ and subsequent images are used as reference images for better editing results.
391+ prompt: `str`
392+ The prompt or prompts to guide image editing. If not defined, you need to pass `prompt_embeds`.
393+ mask: `Optional[Union[str, bytes]]`, optional
394+ An optional mask image. White pixels in the mask are repainted while black pixels are preserved.
395+ If provided, this will trigger inpainting mode. If not provided, this will trigger image-to-image mode.
396+ n: `int`, defaults to 1
397+ The number of images to generate per prompt. Must be between 1 and 10.
398+ size: `Optional[str]`, optional
399+ The width*height in pixels of the generated image. If not specified, uses the original image size.
400+ response_format: `str`, defaults to `url`
401+ The format in which the generated images are returned. Must be one of url or b64_json.
402+ **kwargs
403+ Additional parameters to pass to the model.
404+
405+ Returns
406+ -------
407+ ImageList
408+ A list of edited image objects.
409+
410+ Raises
411+ ------
412+ RuntimeError
413+ If the image editing request fails.
414+
415+ Examples
416+ --------
417+ # Single image editing
418+ result = await model.image_edit(
419+ image="path/to/image.png",
420+ prompt="make this image look like a painting"
421+ )
422+
423+ # Multiple image editing with reference images
424+ result = await model.image_edit(
425+ image=["primary_image.png", "reference1.jpg", "reference2.png"],
426+ prompt="edit the main image using the style from reference images"
427+ )
428+ """
429+ url = f"{ self ._base_url } /v1/images/edits"
430+ params = {
431+ "model" : self ._model_uid ,
432+ "prompt" : prompt ,
433+ "n" : n ,
434+ "size" : size ,
435+ "response_format" : response_format ,
436+ "kwargs" : json .dumps (kwargs ),
437+ }
438+ params = _filter_params (params )
439+ files : List [Any ] = []
440+ for key , value in params .items ():
441+ files .append ((key , (None , value )))
442+
443+ # Handle single image or multiple images
444+ import aiohttp
445+
446+ data = aiohttp .FormData ()
447+
448+ # Add all parameters as form fields
449+ for key , value in params .items ():
450+ if value is not None :
451+ data .add_field (key , str (value ))
452+
453+ # Handle single image or multiple images
454+ if isinstance (image , list ):
455+ # Validate image list is not empty
456+ if len (image ) == 0 :
457+ raise ValueError ("Image list cannot be empty" )
458+ # Multiple images - send as image[] array
459+ for i , img in enumerate (image ):
460+ if isinstance (img , str ):
461+ # File path - read file content
462+ with open (img , "rb" ) as f :
463+ content = f .read ()
464+ data .add_field (
465+ f"image[]" ,
466+ content ,
467+ filename = f"image_{ i } .png" ,
468+ content_type = "image/png" ,
469+ )
470+ else :
471+ # Binary data
472+ data .add_field (
473+ f"image[]" ,
474+ img ,
475+ filename = f"image_{ i } .png" ,
476+ content_type = "image/png" ,
477+ )
478+ else :
479+ # Single image
480+ if isinstance (image , str ):
481+ # File path - read file content
482+ with open (image , "rb" ) as f :
483+ content = f .read ()
484+ data .add_field (
485+ "image" , content , filename = "image.png" , content_type = "image/png"
486+ )
487+ else :
488+ # Binary data
489+ data .add_field (
490+ "image" , image , filename = "image.png" , content_type = "image/png"
491+ )
492+
493+ if mask is not None :
494+ if isinstance (mask , str ):
495+ # File path - read file content
496+ with open (mask , "rb" ) as f :
497+ content = f .read ()
498+ data .add_field (
499+ "mask" , content , filename = "mask.png" , content_type = "image/png"
500+ )
501+ else :
502+ # Binary data
503+ data .add_field (
504+ "mask" , mask , filename = "mask.png" , content_type = "image/png"
505+ )
506+
507+ try :
508+ response = await self .session .post (
509+ url , data = data , headers = self .auth_headers
510+ )
511+ if response .status != 200 :
512+ raise RuntimeError (
513+ f"Failed to edit the images, detail: { await _get_error_string (response )} "
514+ )
515+
516+ response_data = await response .json ()
517+ return response_data
518+ finally :
519+ await _release_response (response ) if "response" in locals () else None
520+
369521 async def inpainting (
370522 self ,
371523 image : Union [str , bytes ],
@@ -436,7 +588,7 @@ async def inpainting(
436588 ("mask_image" , mask_image , "application/octet-stream" ),
437589 )
438590 )
439- response = await self .session .post (url , files = files , headers = self .auth_headers )
591+ response = await self .session .post (url , data = files , headers = self .auth_headers )
440592 if response .status != 200 :
441593 raise RuntimeError (
442594 f"Failed to inpaint the images, detail: { await _get_error_string (response )} "
@@ -457,7 +609,7 @@ async def ocr(self, image: Union[str, bytes], **kwargs):
457609 for key , value in params .items ():
458610 files .append ((key , (None , value )))
459611 files .append (("image" , ("image" , image , "application/octet-stream" )))
460- response = await self .session .post (url , files = files , headers = self .auth_headers )
612+ response = await self .session .post (url , data = files , headers = self .auth_headers )
461613 if response .status != 200 :
462614 raise RuntimeError (
463615 f"Failed to ocr the images, detail: { await _get_error_string (response )} "
@@ -547,7 +699,7 @@ async def image_to_video(
547699 for key , value in params .items ():
548700 files .append ((key , (None , value )))
549701 files .append (("image" , ("image" , image , "application/octet-stream" )))
550- response = await self .session .post (url , files = files , headers = self .auth_headers )
702+ response = await self .session .post (url , data = files , headers = self .auth_headers )
551703 if response .status != 200 :
552704 raise RuntimeError (
553705 f"Failed to create the video from image, detail: { await _get_error_string (response )} "
@@ -987,8 +1139,9 @@ def __init__(self, base_url, api_key: Optional[str] = None):
9871139 self .base_url = base_url
9881140 self ._headers : Dict [str , str ] = {}
9891141 self ._cluster_authed = False
1142+ self .timeout = aiohttp .ClientTimeout (total = 1800 )
9901143 self .session = aiohttp .ClientSession (
991- connector = aiohttp .TCPConnector (force_close = True )
1144+ connector = aiohttp .TCPConnector (force_close = True ), timeout = self . timeout
9921145 )
9931146 self ._check_cluster_authenticated ()
9941147 if api_key is not None and self ._cluster_authed :
0 commit comments