From 6ea7401be7ee5870f37caf0ea84c8665c7af1315 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 4 Mar 2026 22:10:50 -0500 Subject: [PATCH 01/10] feat: photoMaker in imageInference, IIpAdapter (guideImages, etc.), IEmbedding weight, dict coercion for nested options --- runware/base.py | 24 +++++++++++--- runware/types.py | 82 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/runware/base.py b/runware/base.py index ef819d6..bc94654 100644 --- a/runware/base.py +++ b/runware/base.py @@ -734,7 +734,9 @@ async def _imageInference( ip_adapter_data = { k: v for k, v in vars(ip_adapter).items() if v is not None } - if "guideImage" in ip_adapter_data: + if "guideImages" in ip_adapter_data: + ip_adapter_data["guideImages"] = await process_image(ip_adapter_data["guideImages"]) + elif "guideImage" in ip_adapter_data: ip_adapter_data["guideImage"] = await process_image(ip_adapter_data["guideImage"]) ip_adapters_data.append(ip_adapter_data) @@ -762,9 +764,17 @@ async def _imageInference( if requestImage.puLID.inputImages: pulid_data["inputImages"] = await process_image(requestImage.puLID.inputImages) + photo_maker_data = {} + if requestImage.photoMaker: + photo_maker_data = {"images": []} + if requestImage.photoMaker.style is not None: + photo_maker_data["style"] = requestImage.photoMaker.style + if requestImage.photoMaker.images: + photo_maker_data["images"] = await process_image(requestImage.photoMaker.images) + request_object = self._buildImageRequest( requestImage, prompt, control_net_data_dicts, - instant_id_data, ip_adapters_data, ace_plus_plus_data, pulid_data + instant_id_data, ip_adapters_data, ace_plus_plus_data, pulid_data, photo_maker_data ) delivery_method_enum = EDeliveryMethod(requestImage.deliveryMethod) if isinstance(requestImage.deliveryMethod, @@ -2356,7 +2366,7 @@ def is_text_complete(r: "Dict[str, Any]") -> bool: finally: await self._unregister_pending_operation(task_uuid) - def _buildImageRequest(self, requestImage: IImageInference, prompt: Optional[str], control_net_data_dicts: List[Dict], instant_id_data: Optional[Dict], ip_adapters_data: Optional[List[Dict]], ace_plus_plus_data: Optional[Dict], pulid_data: Optional[Dict]) -> Dict[str, Any]: + def _buildImageRequest(self, requestImage: IImageInference, prompt: Optional[str], control_net_data_dicts: List[Dict], instant_id_data: Optional[Dict], ip_adapters_data: Optional[List[Dict]], ace_plus_plus_data: Optional[Dict], pulid_data: Optional[Dict], photo_maker_data: Optional[Dict]) -> Dict[str, Any]: request_object = { "taskType": ETaskType.IMAGE_INFERENCE.value, "taskUUID": requestImage.taskUUID, @@ -2368,7 +2378,7 @@ def _buildImageRequest(self, requestImage: IImageInference, prompt: Optional[str request_object["positivePrompt"] = prompt self._addOptionalImageFields(request_object, requestImage) - self._addImageSpecialFields(request_object, requestImage, control_net_data_dicts, instant_id_data, ip_adapters_data, ace_plus_plus_data, pulid_data) + self._addImageSpecialFields(request_object, requestImage, control_net_data_dicts, instant_id_data, ip_adapters_data, ace_plus_plus_data, pulid_data, photo_maker_data) self._addOptionalField(request_object, requestImage.inputs) self._addImageProviderSettings(request_object, requestImage) self._addOptionalField(request_object, requestImage.ultralytics) @@ -2396,7 +2406,7 @@ def _addOptionalImageFields(self, request_object: Dict[str, Any], requestImage: else: request_object[field] = value - def _addImageSpecialFields(self, request_object: Dict[str, Any], requestImage: IImageInference, control_net_data_dicts: List[Dict], instant_id_data: Optional[Dict], ip_adapters_data: Optional[List[Dict]], ace_plus_plus_data: Optional[Dict], pulid_data: Optional[Dict]) -> None: + def _addImageSpecialFields(self, request_object: Dict[str, Any], requestImage: IImageInference, control_net_data_dicts: List[Dict], instant_id_data: Optional[Dict], ip_adapters_data: Optional[List[Dict]], ace_plus_plus_data: Optional[Dict], pulid_data: Optional[Dict], photo_maker_data: Optional[Dict]) -> None: # Add controlNet if present if control_net_data_dicts: request_object["controlNet"] = control_net_data_dicts @@ -2456,6 +2466,10 @@ def _addImageSpecialFields(self, request_object: Dict[str, Any], requestImage: I if pulid_data: request_object["puLID"] = pulid_data + # Add photoMaker if present + if photo_maker_data: + request_object["photoMaker"] = photo_maker_data + # Add referenceImages if present if requestImage.referenceImages: request_object["referenceImages"] = requestImage.referenceImages diff --git a/runware/types.py b/runware/types.py index 7d32aa8..3d3f4bb 100644 --- a/runware/types.py +++ b/runware/types.py @@ -184,6 +184,7 @@ class ILycoris: @dataclass class IEmbedding: model: str + weight: Optional[float] = None @dataclass @@ -353,22 +354,9 @@ def __post_init__(self): raise ValueError("inputImages can contain a maximum of 4 elements.") # Validate `style` to ensure it matches one of the allowed case-sensitive options - valid_styles = { - "No style", - "Cinematic", - "Disney Character", - "Digital Art", - "Photographic", - "Fantasy art", - "Neonpunk", - "Enhance", - "Comic book", - "Lowpoly", - "Line art", - } - if self.style and self.style not in valid_styles: + if self.style and self.style not in _PHOTO_MAKER_VALID_STYLES: raise ValueError( - f"style must be one of the following: {', '.join(valid_styles)}." + f"style must be one of the following: {', '.join(sorted(_PHOTO_MAKER_VALID_STYLES))}." ) @@ -417,8 +405,13 @@ class IInstantID: @dataclass class IIpAdapter: model: Union[int, str] - guideImage: Union[File, str] + guideImage: Optional[Union[File, str]] = None + guideImages: Optional[List[Union[str, File]]] = None weight: Optional[float] = None + combineMethod: Optional[str] = None + weightType: Optional[str] = None + embedScaling: Optional[str] = None + weightComposition: Optional[float] = None @dataclass @@ -450,6 +443,24 @@ class IPuLID: CFGStartStepPercentage: Optional[int] = None # Min: 0, Max: 100 +_PHOTO_MAKER_VALID_STYLES = {"No style", "Cinematic", "Disney Character", "Digital Art", "Photographic", "Fantasy art", "Neonpunk", "Enhance", "Comic book", "Lowpoly", "Line art"} + + +@dataclass +class IPhotoMakerSettings: + + images: List[Union[str, File]] = field(default_factory=list) + style: Optional[str] = None + + def __post_init__(self): + if len(self.images) > 4: + raise ValueError("photoMaker.images can contain a maximum of 4 elements.") + if self.style and self.style not in _PHOTO_MAKER_VALID_STYLES: + raise ValueError( + f"photoMaker.style must be one of: {', '.join(sorted(_PHOTO_MAKER_VALID_STYLES))}." + ) + + @dataclass class IAcceleratorOptions(SerializableMixin): fbcache: Optional[bool] = None @@ -938,22 +949,23 @@ class IImageInference: lycoris: Optional[List[ILycoris]] = field(default_factory=list) includeCost: Optional[bool] = None onPartialImages: Optional[Callable[[List[IImage], Optional[IError]], None]] = None - refiner: Optional[IRefiner] = None + refiner: Optional[Union[IRefiner, Dict[str, Any]]] = None vae: Optional[str] = None maskMargin: Optional[int] = None outputQuality: Optional[int] = None - embeddings: Optional[List[IEmbedding]] = field(default_factory=list) - outpaint: Optional[IOutpaint] = None - instantID: Optional[IInstantID] = None + embeddings: Optional[List[Union[IEmbedding, Dict[str, Any]]]] = field(default_factory=list) + outpaint: Optional[Union[IOutpaint, Dict[str, Any]]] = None + instantID: Optional[Union[IInstantID, Dict[str, Any]]] = None ipAdapters: Optional[List[IIpAdapter]] = field(default_factory=list) referenceImages: Optional[List[Union[str, File]]] = field(default_factory=list) - acePlusPlus: Optional[IAcePlusPlus] = None - puLID: Optional[IPuLID] = None + acePlusPlus: Optional[Union[IAcePlusPlus, Dict[str, Any]]] = None + puLID: Optional[Union[IPuLID, Dict[str, Any]]] = None + photoMaker: Optional[Union[IPhotoMakerSettings, Dict[str, Any]]] = None providerSettings: Optional[ImageProviderSettings] = None safety: Optional[Union[ISafety, Dict[str, Any]]] = None settings: Optional[Union[ISettings, Dict[str, Any]]] = None inputs: Optional[Union[IInputs, Dict[str, Any]]] = None - ultralytics: Optional[IUltralytics] = None + ultralytics: Optional[Union[IUltralytics, Dict[str, Any]]] = None useCache: Optional[bool] = None resolution: Optional[str] = None extraArgs: Optional[Dict[str, Any]] = field(default_factory=dict) @@ -967,6 +979,30 @@ def __post_init__(self): self.settings = ISettings(**self.settings) if self.inputs is not None and isinstance(self.inputs, dict): self.inputs = IInputs(**self.inputs) + if self.outpaint is not None and isinstance(self.outpaint, dict): + self.outpaint = IOutpaint(**self.outpaint) + if self.refiner is not None and isinstance(self.refiner, dict): + self.refiner = IRefiner(**self.refiner) + if self.embeddings: + self.embeddings = [ + IEmbedding(**item) if isinstance(item, dict) else item + for item in self.embeddings + ] + if self.photoMaker is not None and isinstance(self.photoMaker, dict): + self.photoMaker = IPhotoMakerSettings(**self.photoMaker) + if self.instantID is not None and isinstance(self.instantID, dict): + self.instantID = IInstantID(**self.instantID) + if self.acePlusPlus is not None and isinstance(self.acePlusPlus, dict): + self.acePlusPlus = IAcePlusPlus(**self.acePlusPlus) + if self.puLID is not None and isinstance(self.puLID, dict): + self.puLID = IPuLID(**self.puLID) + if self.ultralytics is not None and isinstance(self.ultralytics, dict): + self.ultralytics = IUltralytics(**self.ultralytics) + if self.ipAdapters: + self.ipAdapters = [ + IIpAdapter(**item) if isinstance(item, dict) else item + for item in self.ipAdapters + ] @dataclass From e4ed80571b2564ba54e888b6800aebcae39fb128 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 5 Mar 2026 15:12:44 -0500 Subject: [PATCH 02/10] Refactored to use IPhotomaker as independent photoMaker or as a param in imageInference --- runware/base.py | 4 ++++ runware/types.py | 45 ++++++++------------------------------------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/runware/base.py b/runware/base.py index bc94654..221e561 100644 --- a/runware/base.py +++ b/runware/base.py @@ -596,6 +596,9 @@ async def photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IIma async def _photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IImage], IAsyncTaskResponse]": await self.ensureConnection() + if requestPhotoMaker.model is None or requestPhotoMaker.positivePrompt is None or requestPhotoMaker.height is None or requestPhotoMaker.width is None: + raise ValueError("Standalone photoMaker requires model, positivePrompt, height, and width.") + task_uuid = requestPhotoMaker.taskUUID or getUUID() requestPhotoMaker.taskUUID = task_uuid @@ -782,6 +785,7 @@ async def _imageInference( task_uuid = requestImage.taskUUID number_results = requestImage.numberResults or 1 + if delivery_method_enum is EDeliveryMethod.ASYNC: if requestImage.webhookURL: request_object["webhookURL"] = requestImage.webhookURL diff --git a/runware/types.py b/runware/types.py index 3d3f4bb..38b8c9e 100644 --- a/runware/types.py +++ b/runware/types.py @@ -333,14 +333,15 @@ def __post_init__(self): @dataclass class IPhotoMaker: - model: Union[int, str] - positivePrompt: str - height: int - width: int + model: Optional[Union[int, str]] = None + positivePrompt: Optional[str] = None + height: Optional[int] = None + width: Optional[int] = None numberResults: int = 1 steps: Optional[int] = None outputType: Optional[IOutputType] = None inputImages: List[Union[str, File]] = field(default_factory=list) + images: Optional[List[Union[str, File]]] = None style: Optional[str] = None strength: Optional[float] = None outputFormat: Optional[IOutputFormat] = None @@ -348,18 +349,6 @@ class IPhotoMaker: taskUUID: Optional[str] = None webhookURL: Optional[str] = None - def __post_init__(self): - # Validate `inputImages` to ensure it has a maximum of 4 elements - if len(self.inputImages) > 4: - raise ValueError("inputImages can contain a maximum of 4 elements.") - - # Validate `style` to ensure it matches one of the allowed case-sensitive options - if self.style and self.style not in _PHOTO_MAKER_VALID_STYLES: - raise ValueError( - f"style must be one of the following: {', '.join(sorted(_PHOTO_MAKER_VALID_STYLES))}." - ) - - class SerializableMixin: def serialize(self) -> Dict[str, Any]: result: Dict[str, Any] = {} @@ -443,24 +432,6 @@ class IPuLID: CFGStartStepPercentage: Optional[int] = None # Min: 0, Max: 100 -_PHOTO_MAKER_VALID_STYLES = {"No style", "Cinematic", "Disney Character", "Digital Art", "Photographic", "Fantasy art", "Neonpunk", "Enhance", "Comic book", "Lowpoly", "Line art"} - - -@dataclass -class IPhotoMakerSettings: - - images: List[Union[str, File]] = field(default_factory=list) - style: Optional[str] = None - - def __post_init__(self): - if len(self.images) > 4: - raise ValueError("photoMaker.images can contain a maximum of 4 elements.") - if self.style and self.style not in _PHOTO_MAKER_VALID_STYLES: - raise ValueError( - f"photoMaker.style must be one of: {', '.join(sorted(_PHOTO_MAKER_VALID_STYLES))}." - ) - - @dataclass class IAcceleratorOptions(SerializableMixin): fbcache: Optional[bool] = None @@ -956,11 +927,11 @@ class IImageInference: embeddings: Optional[List[Union[IEmbedding, Dict[str, Any]]]] = field(default_factory=list) outpaint: Optional[Union[IOutpaint, Dict[str, Any]]] = None instantID: Optional[Union[IInstantID, Dict[str, Any]]] = None - ipAdapters: Optional[List[IIpAdapter]] = field(default_factory=list) + ipAdapters: Optional[List[Union[IIpAdapter, Dict[str, Any]]]] = field(default_factory=list) referenceImages: Optional[List[Union[str, File]]] = field(default_factory=list) acePlusPlus: Optional[Union[IAcePlusPlus, Dict[str, Any]]] = None puLID: Optional[Union[IPuLID, Dict[str, Any]]] = None - photoMaker: Optional[Union[IPhotoMakerSettings, Dict[str, Any]]] = None + photoMaker: Optional[Union[IPhotoMaker, Dict[str, Any]]] = None providerSettings: Optional[ImageProviderSettings] = None safety: Optional[Union[ISafety, Dict[str, Any]]] = None settings: Optional[Union[ISettings, Dict[str, Any]]] = None @@ -989,7 +960,7 @@ def __post_init__(self): for item in self.embeddings ] if self.photoMaker is not None and isinstance(self.photoMaker, dict): - self.photoMaker = IPhotoMakerSettings(**self.photoMaker) + self.photoMaker = IPhotoMaker(**self.photoMaker) if self.instantID is not None and isinstance(self.instantID, dict): self.instantID = IInstantID(**self.instantID) if self.acePlusPlus is not None and isinstance(self.acePlusPlus, dict): From c0d9aa5cc1768eea2e42f6804c4d849d9d594a2f Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 5 Mar 2026 15:17:33 -0500 Subject: [PATCH 03/10] Updated _addImageSpecialFields for embedding in imageInference --- runware/base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/runware/base.py b/runware/base.py index 221e561..a3074a4 100644 --- a/runware/base.py +++ b/runware/base.py @@ -2431,10 +2431,13 @@ def _addImageSpecialFields(self, request_object: Dict[str, Any], requestImage: I # Add embeddings if present if requestImage.embeddings: - request_object["embeddings"] = [ - {"model": embedding.model} - for embedding in requestImage.embeddings - ] + embeddings_payload = [] + for embedding in requestImage.embeddings: + d: Dict[str, Any] = {"model": embedding.model} + if embedding.weight is not None: + d["weight"] = embedding.weight + embeddings_payload.append(d) + request_object["embeddings"] = embeddings_payload # Add refiner if present if requestImage.refiner: From c554ad747d3949498616f0949ba88f5b1fcdc62c Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 5 Mar 2026 15:55:09 -0500 Subject: [PATCH 04/10] Add inputImages to instantID --- runware/base.py | 19 ++++++++++++------- runware/types.py | 5 +++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/runware/base.py b/runware/base.py index a3074a4..4b681f0 100644 --- a/runware/base.py +++ b/runware/base.py @@ -726,10 +726,12 @@ async def _imageInference( for k, v in vars(requestImage.instantID).items() if v is not None } - if "inputImage" in instant_id_data: - instant_id_data["inputImage"] = await process_image(instant_id_data["inputImage"]) if "poseImage" in instant_id_data: instant_id_data["poseImage"] = await process_image(instant_id_data["poseImage"]) + if "inputImages" in instant_id_data: + instant_id_data["inputImages"] = await process_image(instant_id_data["inputImages"]) + else: + instant_id_data["inputImages"] = await process_image([instant_id_data["inputImage"]]) ip_adapters_data = [] if requestImage.ipAdapters: @@ -769,11 +771,14 @@ async def _imageInference( photo_maker_data = {} if requestImage.photoMaker: - photo_maker_data = {"images": []} - if requestImage.photoMaker.style is not None: - photo_maker_data["style"] = requestImage.photoMaker.style - if requestImage.photoMaker.images: - photo_maker_data["images"] = await process_image(requestImage.photoMaker.images) + photo_maker_data = { + k: v for k, v in vars(requestImage.photoMaker).items() + if v is not None and not (k in ("images", "inputImages") and v == []) + } + if "images" in photo_maker_data: + photo_maker_data["images"] = await process_image(photo_maker_data["images"]) + if "inputImages" in photo_maker_data: + photo_maker_data["inputImages"] = await process_image(photo_maker_data["inputImages"]) request_object = self._buildImageRequest( requestImage, prompt, control_net_data_dicts, diff --git a/runware/types.py b/runware/types.py index 38b8c9e..5ee305e 100644 --- a/runware/types.py +++ b/runware/types.py @@ -340,7 +340,7 @@ class IPhotoMaker: numberResults: int = 1 steps: Optional[int] = None outputType: Optional[IOutputType] = None - inputImages: List[Union[str, File]] = field(default_factory=list) + inputImages: Optional[List[Union[str, File]]] = None images: Optional[List[Union[str, File]]] = None style: Optional[str] = None strength: Optional[float] = None @@ -382,8 +382,9 @@ class IOutpaint: @dataclass class IInstantID: - inputImage: Union[File, str] + inputImage: Optional[Union[File, str]] = None poseImage: Optional[Union[File, str]] = None + inputImages: Optional[List[Union[str, File]]] = None identityNetStrength: Optional[float] = None adapterStrength: Optional[float] = None controlNetCannyWeight: Optional[float] = None From ca6ed1d64d05c30446e1c25345683481fab8feb5 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 5 Mar 2026 21:19:46 -0500 Subject: [PATCH 05/10] Add more fields to IPhotoMaker --- runware/base.py | 29 +++++++++++++++++++++-------- runware/types.py | 7 ++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/runware/base.py b/runware/base.py index 4b681f0..84bdc18 100644 --- a/runware/base.py +++ b/runware/base.py @@ -635,6 +635,16 @@ async def _photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IIm request_object["includeCost"] = requestPhotoMaker.includeCost if requestPhotoMaker.outputType: request_object["outputType"] = requestPhotoMaker.outputType + if requestPhotoMaker.negativePrompt is not None: + request_object["negativePrompt"] = requestPhotoMaker.negativePrompt + if requestPhotoMaker.CFGScale is not None: + request_object["CFGScale"] = requestPhotoMaker.CFGScale + if requestPhotoMaker.seed is not None: + request_object["seed"] = requestPhotoMaker.seed + if requestPhotoMaker.scheduler is not None: + request_object["scheduler"] = requestPhotoMaker.scheduler + if requestPhotoMaker.checkNsfw is not None: + request_object["checkNSFW"] = requestPhotoMaker.checkNsfw if requestPhotoMaker.webhookURL: request_object["webhookURL"] = requestPhotoMaker.webhookURL return await self._handleWebhookRequest( @@ -646,6 +656,9 @@ async def _photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IIm numberOfResults = requestPhotoMaker.numberResults + import json + print(f"request_object: {json.dumps(request_object, indent=4)}") + future, should_send = await self._register_pending_operation( task_uuid, expected_results=numberOfResults, @@ -771,14 +784,12 @@ async def _imageInference( photo_maker_data = {} if requestImage.photoMaker: - photo_maker_data = { - k: v for k, v in vars(requestImage.photoMaker).items() - if v is not None and not (k in ("images", "inputImages") and v == []) - } - if "images" in photo_maker_data: - photo_maker_data["images"] = await process_image(photo_maker_data["images"]) - if "inputImages" in photo_maker_data: - photo_maker_data["inputImages"] = await process_image(photo_maker_data["inputImages"]) + photo_maker_data = {} + if requestImage.photoMaker.style is not None: + photo_maker_data["style"] = requestImage.photoMaker.style + image_list = requestImage.photoMaker.images or requestImage.photoMaker.inputImages + if image_list: + photo_maker_data["images"] = await process_image(image_list) request_object = self._buildImageRequest( requestImage, prompt, control_net_data_dicts, @@ -790,6 +801,8 @@ async def _imageInference( task_uuid = requestImage.taskUUID number_results = requestImage.numberResults or 1 + import json + print(f"request_object1: {json.dumps(request_object, indent=4)}") if delivery_method_enum is EDeliveryMethod.ASYNC: if requestImage.webhookURL: diff --git a/runware/types.py b/runware/types.py index 5ee305e..7c123ca 100644 --- a/runware/types.py +++ b/runware/types.py @@ -337,7 +337,7 @@ class IPhotoMaker: positivePrompt: Optional[str] = None height: Optional[int] = None width: Optional[int] = None - numberResults: int = 1 + numberResults: Optional[int] = 1 steps: Optional[int] = None outputType: Optional[IOutputType] = None inputImages: Optional[List[Union[str, File]]] = None @@ -348,6 +348,11 @@ class IPhotoMaker: includeCost: Optional[bool] = None taskUUID: Optional[str] = None webhookURL: Optional[str] = None + negativePrompt: Optional[str] = None + CFGScale: Optional[float] = None + seed: Optional[int] = None + scheduler: Optional[str] = None + checkNsfw: Optional[bool] = None class SerializableMixin: def serialize(self) -> Dict[str, Any]: From beb4b52d42dff87d72922b131ccbca19a48e1ad7 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 5 Mar 2026 22:02:53 -0500 Subject: [PATCH 06/10] Fixed imageCaption --- runware/base.py | 18 ++++++++---------- runware/types.py | 15 +++++++++++---- runware/utils.py | 16 ---------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/runware/base.py b/runware/base.py index 84bdc18..923b01f 100644 --- a/runware/base.py +++ b/runware/base.py @@ -65,7 +65,6 @@ getUUID, fileToBase64, createImageFromResponse, - createImageToTextFromResponse, createEnhancedPromptsFromResponse, instantiateDataclassList, RunwareAPIError, @@ -599,6 +598,11 @@ async def _photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IIm if requestPhotoMaker.model is None or requestPhotoMaker.positivePrompt is None or requestPhotoMaker.height is None or requestPhotoMaker.width is None: raise ValueError("Standalone photoMaker requires model, positivePrompt, height, and width.") + input_images = requestPhotoMaker.inputImages if requestPhotoMaker.inputImages is not None else [] + if not input_images: + raise ValueError("Standalone photoMaker requires at least one image in inputImages.") + requestPhotoMaker.inputImages = input_images + task_uuid = requestPhotoMaker.taskUUID or getUUID() requestPhotoMaker.taskUUID = task_uuid @@ -656,8 +660,6 @@ async def _photoMaker(self, requestPhotoMaker: "IPhotoMaker") -> "Union[List[IIm numberOfResults = requestPhotoMaker.numberResults - import json - print(f"request_object: {json.dumps(request_object, indent=4)}") future, should_send = await self._register_pending_operation( task_uuid, @@ -801,9 +803,6 @@ async def _imageInference( task_uuid = requestImage.taskUUID number_results = requestImage.numberResults or 1 - import json - print(f"request_object1: {json.dumps(request_object, indent=4)}") - if delivery_method_enum is EDeliveryMethod.ASYNC: if requestImage.webhookURL: request_object["webhookURL"] = requestImage.webhookURL @@ -935,9 +934,8 @@ async def _requestImageToText( # Add template parameter if specified if requestImageToText.template is not None: task_params["template"] = requestImageToText.template - # When using template, do NOT include prompt parameter - else: - # Use the provided prompt when no template + # Add prompt only when explicitly provided (API does not support prompt in all cases) + elif requestImageToText.prompt is not None: task_params["prompt"] = requestImageToText.prompt # Add optional parameters if they are provided @@ -965,7 +963,7 @@ async def _requestImageToText( results = await asyncio.wait_for(future, timeout=IMAGE_OPERATION_TIMEOUT / 1000) response = results[0] self._handle_error_response(response) - return createImageToTextFromResponse(response) + return instantiateDataclass(IImageToText, response) except asyncio.TimeoutError: raise Exception( f"Timeout waiting for image caption | TaskUUID: {taskUUID} | " diff --git a/runware/types.py b/runware/types.py index 7c123ca..792b6ad 100644 --- a/runware/types.py +++ b/runware/types.py @@ -184,7 +184,7 @@ class ILycoris: @dataclass class IEmbedding: model: str - weight: Optional[float] = None + weight: Optional[float] = None @dataclass @@ -337,7 +337,7 @@ class IPhotoMaker: positivePrompt: Optional[str] = None height: Optional[int] = None width: Optional[int] = None - numberResults: Optional[int] = 1 + numberResults: Optional[int] = None steps: Optional[int] = None outputType: Optional[IOutputType] = None inputImages: Optional[List[Union[str, File]]] = None @@ -986,7 +986,7 @@ def __post_init__(self): class IImageCaption: inputImages: Optional[List[Union[File, str]]] = None # Primary: array of images (UUIDs, URLs, base64, dataURI) inputImage: Optional[Union[File, str]] = None # Convenience: single image, defaults to inputImages[0] if not provided - prompt: List[str] = field(default_factory=lambda: ["Describe this image in detail"]) # Array of prompts with default + prompt: Optional[List[str]] = None model: Optional[str] = None # Optional: AIR ID (runware:150@1, runware:150@2) - backend handles default includeCost: bool = False template: Optional[str] = None @@ -1025,11 +1025,18 @@ class IElevenLabsMusicSettings(SerializableMixin): compositionPlan: IElevenLabsCompositionPlan # Music composition structure +@dataclass +class IImageToTextStructuredData: + ageGroup: Optional[str] = None + confidence: Optional[float] = None + + @dataclass class IImageToText: taskType: ETaskType taskUUID: str - text: str + text: Optional[str] = None + structuredData: Optional[IImageToTextStructuredData] = None cost: Optional[float] = None diff --git a/runware/utils.py b/runware/utils.py index f590ffe..d765700 100644 --- a/runware/utils.py +++ b/runware/utils.py @@ -767,22 +767,6 @@ def createImageFromResponse(response: dict) -> IImage: return instantiateDataclass(IImage, processed_fields) -def createImageToTextFromResponse(response: dict) -> IImageToText: - processed_fields = {} - - for field in fields(IImageToText): - if field.name in response: - if field.name == "taskType": - # Convert string to ETaskType enum - processed_fields[field.name] = ETaskType(response[field.name]) - elif field.type == float or field.type == Optional[float]: - processed_fields[field.name] = float(response[field.name]) - else: - processed_fields[field.name] = response[field.name] - - return instantiateDataclass(IImageToText, processed_fields) - - def createVideoToTextFromResponse(response: dict) -> IVideoToText: processed_fields = {} From b0544dd6af433d55809b24230db5f5c6c170cb33 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 12 Mar 2026 16:55:06 -0400 Subject: [PATCH 07/10] Fixed instantiateDataclassList() --- runware/base.py | 16 ++++++++++++---- runware/utils.py | 12 +++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/runware/base.py b/runware/base.py index 923b01f..d2f8d60 100644 --- a/runware/base.py +++ b/runware/base.py @@ -741,12 +741,20 @@ async def _imageInference( for k, v in vars(requestImage.instantID).items() if v is not None } + if "poseImage" in instant_id_data: instant_id_data["poseImage"] = await process_image(instant_id_data["poseImage"]) - if "inputImages" in instant_id_data: - instant_id_data["inputImages"] = await process_image(instant_id_data["inputImages"]) - else: - instant_id_data["inputImages"] = await process_image([instant_id_data["inputImage"]]) + + input_images = instant_id_data.get("inputImages") + single_input = instant_id_data.get("inputImage") + + if input_images is None and single_input is not None: + input_images = [single_input] + + if input_images is not None: + instant_id_data["inputImages"] = await process_image(input_images) + + instant_id_data.pop("inputImage", None) ip_adapters_data = [] if requestImage.ipAdapters: diff --git a/runware/utils.py b/runware/utils.py index d765700..f314319 100644 --- a/runware/utils.py +++ b/runware/utils.py @@ -13,6 +13,7 @@ import inspect from typing import Any, Dict, List, Union, Optional, TypeVar, Type, Coroutine, get_type_hints, get_origin, get_args from dataclasses import fields, is_dataclass +from enum import Enum from .types import ( Environment, EPreProcessor, @@ -906,9 +907,18 @@ def instantiateDataclass(dataclass_type: Type[Any], data: dict) -> Any: elif get_origin(field_type) is list and isinstance(v, list): inner = get_args(field_type)[0] if get_args(field_type) else None if inner and is_dataclass(inner): - filtered_data[k] = [instantiateDataclass(inner, i) if isinstance(i, dict) else i for i in v] + filtered_data[k] = [ + instantiateDataclass(inner, i) if isinstance(i, dict) else i + for i in v + ] else: filtered_data[k] = v + + elif isinstance(field_type, type) and issubclass(field_type, Enum): + filtered_data[k] = field_type(v) + + elif field_type in (int, float) and isinstance(v, str): + filtered_data[k] = field_type(v) else: filtered_data[k] = v From 2ba44ce60fd95139621961503cd87f4cd7ad4daa Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 12 Mar 2026 16:59:54 -0400 Subject: [PATCH 08/10] Fixed same simple assignment in instantiateDataclass --- runware/base.py | 1 - runware/utils.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/runware/base.py b/runware/base.py index d2f8d60..4520469 100644 --- a/runware/base.py +++ b/runware/base.py @@ -794,7 +794,6 @@ async def _imageInference( photo_maker_data = {} if requestImage.photoMaker: - photo_maker_data = {} if requestImage.photoMaker.style is not None: photo_maker_data["style"] = requestImage.photoMaker.style image_list = requestImage.photoMaker.images or requestImage.photoMaker.inputImages diff --git a/runware/utils.py b/runware/utils.py index f314319..55661d2 100644 --- a/runware/utils.py +++ b/runware/utils.py @@ -914,10 +914,10 @@ def instantiateDataclass(dataclass_type: Type[Any], data: dict) -> Any: else: filtered_data[k] = v - elif isinstance(field_type, type) and issubclass(field_type, Enum): - filtered_data[k] = field_type(v) - - elif field_type in (int, float) and isinstance(v, str): + elif ( + (isinstance(field_type, type) and issubclass(field_type, Enum)) + or (field_type in (int, float) and isinstance(v, str)) + ): filtered_data[k] = field_type(v) else: filtered_data[k] = v From b1ea0ed5e5f25728beabae4ed573f7ccfe7fb13a Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Tue, 17 Mar 2026 14:35:09 -0400 Subject: [PATCH 09/10] Refactored to use PhotoMakerSettings in imageInference --- runware/types.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/runware/types.py b/runware/types.py index 01de3a3..09dde5e 100644 --- a/runware/types.py +++ b/runware/types.py @@ -333,11 +333,11 @@ def __post_init__(self): @dataclass class IPhotoMaker: - model: Optional[Union[int, str]] = None - positivePrompt: Optional[str] = None - height: Optional[int] = None - width: Optional[int] = None - numberResults: Optional[int] = None + model: Union[int, str] + positivePrompt: str + height: int + width: int + numberResults: int = 1 steps: Optional[int] = None outputType: Optional[IOutputType] = None inputImages: Optional[List[Union[str, File]]] = None @@ -354,6 +354,14 @@ class IPhotoMaker: scheduler: Optional[str] = None checkNsfw: Optional[bool] = None + +@dataclass +class IPhotoMakerSettings: + images: Optional[List[Union[str, File]]] = None + inputImages: Optional[List[Union[str, File]]] = None + style: Optional[str] = None + strength: Optional[float] = None + class SerializableMixin: def serialize(self) -> Dict[str, Any]: result: Dict[str, Any] = {} @@ -1004,7 +1012,7 @@ class IImageInference: referenceImages: Optional[List[Union[str, File]]] = field(default_factory=list) acePlusPlus: Optional[Union[IAcePlusPlus, Dict[str, Any]]] = None puLID: Optional[Union[IPuLID, Dict[str, Any]]] = None - photoMaker: Optional[Union[IPhotoMaker, Dict[str, Any]]] = None + photoMaker: Optional[Union[IPhotoMakerSettings, Dict[str, Any]]] = None providerSettings: Optional[ImageProviderSettings] = None safety: Optional[Union[ISafety, Dict[str, Any]]] = None settings: Optional[Union[ISettings, Dict[str, Any]]] = None @@ -1046,7 +1054,7 @@ def __post_init__(self, checkNsfw: Optional[bool] = None): for item in self.embeddings ] if self.photoMaker is not None and isinstance(self.photoMaker, dict): - self.photoMaker = IPhotoMaker(**self.photoMaker) + self.photoMaker = IPhotoMakerSettings(**self.photoMaker) if self.instantID is not None and isinstance(self.instantID, dict): self.instantID = IInstantID(**self.instantID) if self.acePlusPlus is not None and isinstance(self.acePlusPlus, dict): From f6a66c3da6b5fac53635e16ac008c58057fa404d Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Tue, 17 Mar 2026 14:53:31 -0400 Subject: [PATCH 10/10] Fixed photomaker --- runware/base.py | 5 ++++- runware/types.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/runware/base.py b/runware/base.py index 7479fd8..946194b 100644 --- a/runware/base.py +++ b/runware/base.py @@ -793,10 +793,13 @@ async def _imageInference( if requestImage.puLID.inputImages: pulid_data["inputImages"] = await process_image(requestImage.puLID.inputImages) - photo_maker_data = {} + photo_maker_data: Dict[str, Any] = {} if requestImage.photoMaker: if requestImage.photoMaker.style is not None: photo_maker_data["style"] = requestImage.photoMaker.style + if requestImage.photoMaker.strength is not None: + photo_maker_data["strength"] = requestImage.photoMaker.strength + image_list = requestImage.photoMaker.images or requestImage.photoMaker.inputImages if image_list: photo_maker_data["images"] = await process_image(image_list) diff --git a/runware/types.py b/runware/types.py index 09dde5e..635a5d1 100644 --- a/runware/types.py +++ b/runware/types.py @@ -341,7 +341,6 @@ class IPhotoMaker: steps: Optional[int] = None outputType: Optional[IOutputType] = None inputImages: Optional[List[Union[str, File]]] = None - images: Optional[List[Union[str, File]]] = None style: Optional[str] = None strength: Optional[float] = None outputFormat: Optional[IOutputFormat] = None