123 lines
4.2 KiB
Python
123 lines
4.2 KiB
Python
"""Immutable, data-only workflow contracts; independent of ComfyUI."""
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
from .validation import boolean, choice, integer, number, text
|
|
from .editor_profiles import ASPECT_OPTIONS
|
|
|
|
RESOLUTION_POLICIES = ("Auto (preserve detail)", "Auto (closest scale)", "Force 1 MP", "Force 1.5 MP", "Force 2K (~4 MP)")
|
|
PADDING_FILLS = ("Edge extension", "Black", "White")
|
|
GRAVITIES = ("center", "left", "right", "top", "bottom", "top left", "top right", "bottom left", "bottom right")
|
|
|
|
CROP_CONFIG_TYPE = "PORTRAIT_CROP_CONFIG_V2"
|
|
|
|
EDIT_CONFIG_TYPE = "PORTRAIT_EDIT_CONFIG_V2"
|
|
|
|
OUTPAINT_CONFIG_TYPE = "PORTRAIT_OUTPAINT_CONFIG_V2"
|
|
|
|
FINISH_CONFIG_TYPE = "PORTRAIT_FINISH_CONFIG_V2"
|
|
|
|
SOURCE_INFO_TYPE = "PORTRAIT_SOURCE_INFO_V2"
|
|
|
|
@dataclass(frozen=True)
|
|
class CropConfigV2:
|
|
autocrop_strip_bottom_banner: bool
|
|
autocrop_detect_borders: bool
|
|
autocrop_fuzz_tolerance: float
|
|
autocrop_edge_uniformity: float
|
|
autocrop_pad_px: int
|
|
smart_crop: bool
|
|
resolution_policy: str
|
|
forced_aspect_ratio: str
|
|
subject_mask: str
|
|
padding_fill: str = "Edge extension"
|
|
|
|
@dataclass(frozen=True)
|
|
class EditConfigV2:
|
|
inpaint_prompt: str
|
|
blend_opacity: float
|
|
stitch_opacity: float
|
|
stitch_prompt_1: str
|
|
stitch_prompt_2: str = ""
|
|
stitch_prompt_3: str = ""
|
|
|
|
@dataclass(frozen=True)
|
|
class OutpaintConfigV2:
|
|
mode: str
|
|
gravity: str
|
|
horizontal_percent: float
|
|
vertical_percent: float
|
|
left_px: int
|
|
right_px: int
|
|
top_px: int
|
|
bottom_px: int
|
|
outpaint_prompt: str
|
|
|
|
@dataclass(frozen=True)
|
|
class FinishConfigV2:
|
|
initial_upscale: float
|
|
final_upscale: float
|
|
auto_levels: bool
|
|
auto_tone: bool
|
|
auto_color: bool
|
|
flip_horizontal: bool
|
|
seed: int
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceInfoV2:
|
|
filename_no_ext: str
|
|
output_dir: str
|
|
width: int
|
|
height: int
|
|
|
|
def _expect(value: Any, expected_type, label: str):
|
|
if not isinstance(value, expected_type):
|
|
raise TypeError(
|
|
f"{label} expected {expected_type.__name__}; received "
|
|
f"{type(value).__name__}. Connect the matching PortraitUtils V2 config node."
|
|
)
|
|
return validate_config(value, label)
|
|
|
|
|
|
def validate_config(value, label="config"):
|
|
"""Validate at producer/consumer boundaries, not in dataclass construction."""
|
|
def field(name):
|
|
return getattr(value, name), f"{label}.{name}"
|
|
|
|
if isinstance(value, CropConfigV2):
|
|
for name in ("autocrop_strip_bottom_banner", "autocrop_detect_borders", "smart_crop"):
|
|
boolean(*field(name))
|
|
number(*field("autocrop_fuzz_tolerance"), 0, 0.5)
|
|
number(*field("autocrop_edge_uniformity"), 0, 1)
|
|
integer(*field("autocrop_pad_px"), 0, 256)
|
|
choice(*field("resolution_policy"), RESOLUTION_POLICIES)
|
|
choice(*field("forced_aspect_ratio"), ASPECT_OPTIONS)
|
|
text(*field("subject_mask"))
|
|
choice(*field("padding_fill"), PADDING_FILLS)
|
|
elif isinstance(value, EditConfigV2):
|
|
for name in ("blend_opacity", "stitch_opacity"):
|
|
number(*field(name), 0, 1)
|
|
for name in ("inpaint_prompt", "stitch_prompt_1", "stitch_prompt_2", "stitch_prompt_3"):
|
|
text(*field(name))
|
|
elif isinstance(value, OutpaintConfigV2):
|
|
choice(*field("mode"), ("Percent", "Pixels"))
|
|
choice(*field("gravity"), GRAVITIES)
|
|
for name in ("horizontal_percent", "vertical_percent"):
|
|
number(*field(name), 0, 10000)
|
|
for name in ("left_px", "right_px", "top_px", "bottom_px"):
|
|
integer(*field(name), 0, 1000000)
|
|
text(*field("outpaint_prompt"))
|
|
elif isinstance(value, FinishConfigV2):
|
|
for name in ("initial_upscale", "final_upscale"):
|
|
number(*field(name), 0.1, 100)
|
|
for name in ("auto_levels", "auto_tone", "auto_color", "flip_horizontal"):
|
|
boolean(*field(name))
|
|
integer(*field("seed"), 0, 0xffffffff)
|
|
elif isinstance(value, SourceInfoV2):
|
|
for name in ("filename_no_ext", "output_dir"):
|
|
text(*field(name))
|
|
for name in ("width", "height"):
|
|
integer(*field(name), 1, 2**31 - 1)
|
|
else:
|
|
raise TypeError(f"{label}: unsupported config type {type(value).__name__}.")
|
|
return value
|