"""Validated request and response types independent of MCP and Diffusers.""" from __future__ import annotations from dataclasses import dataclass from enum import StrEnum from typing import Any from .errors import InvalidRequestError class ImageSize(StrEnum): SQUARE_512 = "square_512" LANDSCAPE_512 = "landscape_512" PORTRAIT_512 = "portrait_512" @property def dimensions(self) -> tuple[int, int]: return { ImageSize.SQUARE_512: (512, 512), ImageSize.LANDSCAPE_512: (512, 384), ImageSize.PORTRAIT_512: (384, 512), }[self] class ControlMode(StrEnum): DEPTH = "depth" OPENPOSE = "openpose" OPENPOSE_FULL = "openpose_full" def _prompt(value: Any, label: str, *, required: bool) -> str: if value is None and not required: return "" if not isinstance(value, str): raise InvalidRequestError(f"{label} must be a string") value = value.strip() if required and not value: raise InvalidRequestError(f"{label} must not be empty") if len(value) > 10_000: raise InvalidRequestError(f"{label} is too long") return value def _size(value: str | ImageSize | None, default: str) -> ImageSize: try: return ImageSize(value or default) except ValueError as exc: raise InvalidRequestError(f"unsupported size: {value}") from exc def _combine(base: str, user: str, *, separator: str) -> str: if not base: return user if not user: return base return f"{base}{separator}{user}" @dataclass(frozen=True) class TextToImageRequest: positive_prompt: str negative_prompt: str size: ImageSize @classmethod def create( cls, positive_prompt: str, negative_prompt: str | None = None, size: str | ImageSize | None = None, *, default_size: str = ImageSize.SQUARE_512, base_positive: str = "", base_negative: str = "", ) -> TextToImageRequest: user_positive = _prompt(positive_prompt, "positive_prompt", required=True) user_negative = _prompt(negative_prompt, "negative_prompt", required=False) return cls( _combine(base_positive, user_positive, separator=", "), _combine(base_negative, user_negative, separator="\n"), _size(size, default_size), ) @dataclass(frozen=True) class GuidedImageRequest: positive_prompt: str negative_prompt: str size: ImageSize control_mode: ControlMode input_image: str @classmethod def create( cls, positive_prompt: str, control_mode: str | ControlMode, input_image: str, negative_prompt: str | None = None, size: str | ImageSize | None = None, *, default_size: str = ImageSize.SQUARE_512, base_positive: str = "", base_negative: str = "", ) -> GuidedImageRequest: base = TextToImageRequest.create( positive_prompt, negative_prompt, size, default_size=default_size, base_positive=base_positive, base_negative=base_negative, ) try: mode = ControlMode(control_mode) except ValueError as exc: raise InvalidRequestError(f"unsupported control_mode: {control_mode}") from exc if not isinstance(input_image, str) or not input_image.strip(): raise InvalidRequestError("input_image must be a non-empty Base64 string") return cls(base.positive_prompt, base.negative_prompt, base.size, mode, input_image) @dataclass(frozen=True) class InpaintImageRequest: positive_prompt: str negative_prompt: str size: ImageSize input_image: str mask_image: str @classmethod def create( cls, positive_prompt: str, input_image: str, mask_image: str, negative_prompt: str | None = None, size: str | ImageSize | None = None, *, default_size: str = ImageSize.SQUARE_512, base_positive: str = "", base_negative: str = "", ) -> InpaintImageRequest: base = TextToImageRequest.create( positive_prompt, negative_prompt, size, default_size=default_size, base_positive=base_positive, base_negative=base_negative, ) for label, value in (("input_image", input_image), ("mask_image", mask_image)): if not isinstance(value, str) or not value.strip(): raise InvalidRequestError(f"{label} must be a non-empty Base64 string") return cls( base.positive_prompt, base.negative_prompt, base.size, input_image, mask_image, ) @dataclass(frozen=True) class GenerationResult: png_data: bytes metadata: dict[str, str | int | float]