diff --git a/runware/base.py b/runware/base.py index ef819d6..2344886 100644 --- a/runware/base.py +++ b/runware/base.py @@ -2462,15 +2462,7 @@ def _addImageSpecialFields(self, request_object: Dict[str, Any], requestImage: I # Add acceleratorOptions if present self._addOptionalField(request_object, requestImage.acceleratorOptions) - - # Add advancedFeatures if present - if requestImage.advancedFeatures: - pipeline_options_dict = { - k: v.__dict__ - for k, v in vars(requestImage.advancedFeatures).items() - if v is not None - } - request_object["advancedFeatures"] = pipeline_options_dict + self._addOptionalField(request_object, requestImage.advancedFeatures) # Add extraArgs if present if hasattr(requestImage, "extraArgs") and isinstance(requestImage.extraArgs, dict): diff --git a/runware/types.py b/runware/types.py index 7d32aa8..3da8251 100644 --- a/runware/types.py +++ b/runware/types.py @@ -472,14 +472,70 @@ def request_key(self) -> str: @dataclass -class IFluxKontext: +class IFluxKontext(SerializableMixin): guidanceEndStep: Optional[int] = None guidanceEndStepPercentage: Optional[float] = None + @property + def request_key(self) -> str: + return "fluxKontext" + + +@dataclass +class IRegion(SerializableMixin): + prompt: str + mask: Union[List[int], str] + + @property + def request_key(self) -> str: + return "regions" + + def __post_init__(self) -> None: + if isinstance(self.mask, list): + if len(self.mask) != 4: + raise ValueError("IRegion.mask must be a list of exactly 4 integers [x0, y0, x1, y1]") + if not all(isinstance(v, int) for v in self.mask): + raise TypeError("IRegion.mask list elements must all be ints") + + +@dataclass +class IRegionalPrompting(SerializableMixin): + injectSteps: int + backgroundPrompt: Optional[str] = None + baseRatio: Optional[float] = None + regions: Optional[List[IRegion]] = None + + @property + def request_key(self) -> str: + return "regionalPrompting" + + +@dataclass +class IWatermark(SerializableMixin): + text: Optional[str] = None + image: Optional[str] = None + displayPosition: Optional[str] = None + tiled: Optional[bool] = None + opacity: Optional[float] = None + fontColor: Optional[str] = None + bgColor: Optional[str] = None + + @property + def request_key(self) -> str: + return "watermark" + @dataclass -class IAdvancedFeatures: +class IAdvancedFeatures(SerializableMixin): fluxKontext: Optional[IFluxKontext] = None + layerDiffuse: Optional[bool] = None + hiresFix: Optional[bool] = None + regionalPrompting: Optional[IRegionalPrompting] = None + watermark: Optional[IWatermark] = None + + @property + def request_key(self) -> str: + return "advancedFeatures" @dataclass @@ -505,20 +561,23 @@ class IVideoAdvancedFeatures(SerializableMixin): audioNegativePrompt: Optional[str] = None slgLayer: Optional[int] = None advancedFeature: Optional[VideoAdvancedFeatureTypes] = None + watermark: Optional[IWatermark] = None @property def request_key(self) -> str: return "advancedFeatures" def serialize(self) -> Dict[str, Any]: - result = {k: v for k, v in asdict(self).items() - if v is not None and not k.startswith('_')} - - - if self.advancedFeature: - result.pop('advancedFeature', None) - result.update(self.advancedFeature.to_request_dict()) - + result: Dict[str, Any] = {} + for k, v in vars(self).items(): + if v is None or k.startswith("_"): + continue + if isinstance(v, SerializableMixin): + result.update(v.to_request_dict()) + elif isinstance(v, (list, tuple)) and v and all(isinstance(x, SerializableMixin) for x in v): + result[k] = [x.serialize() for x in v] + else: + result[k] = v return result