MpakoImageAIbot/workflows.py

152 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import copy
import json
import math
from collections.abc import Callable
from pathlib import Path
from typing import Any
RoleField = tuple[str | None, Callable[[Any], bool] | None]
ROLE_FIELDS: dict[str, dict[str, RoleField]] = {
"txt2img": {
"positive": ("text", lambda value: isinstance(value, str)),
"negative": ("text", lambda value: isinstance(value, str)),
"seed": ("seed", lambda value: isinstance(value, int)),
"latent": (None, None),
"save": (None, None),
},
"img2img": {
"positive": ("prompt", lambda value: isinstance(value, str)),
"negative": ("text", lambda value: isinstance(value, str)),
"seed": ("noise_seed", lambda value: isinstance(value, int)),
"input_image": ("image", lambda value: isinstance(value, str)),
"latent": (None, None),
"save": (None, None),
},
}
LATENT_CLASSES = {"EmptySD3LatentImage", "EmptyLatentImage", "EmptyLatentImagePresets"}
def fit_latent_size(
width: int,
height: int,
*,
target_pixels: int = 1024 * 1024,
multiple: int = 16,
max_side: int = 1536,
) -> tuple[int, int]:
if width <= 0 or height <= 0:
return 1024, 1024
ratio = math.sqrt(target_pixels / (width * height))
new_width = width * ratio
new_height = height * ratio
longest = max(new_width, new_height)
if longest > max_side:
scale = max_side / longest
new_width *= scale
new_height *= scale
new_width = max(multiple, round(new_width / multiple) * multiple)
new_height = max(multiple, round(new_height / multiple) * multiple)
return int(new_width), int(new_height)
class WorkflowError(RuntimeError):
pass
class WorkflowBuilder:
def __init__(self, t2i_path: Path, i2i_path: Path, mapping_path: Path) -> None:
self._paths = {"txt2img": t2i_path, "img2img": i2i_path}
self._templates = {mode: self._load_json(path) for mode, path in self._paths.items()}
self._mapping = self._load_json(mapping_path)
self._validate()
@staticmethod
def _load_json(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise WorkflowError(f"Не найден workflow: {path}") from exc
except json.JSONDecodeError as exc:
raise WorkflowError(f"Некорректный JSON в {path}: {exc}") from exc
if not isinstance(data, dict) or not data:
raise WorkflowError(f"Workflow {path} должен быть непустым объектом")
return data
def _validate(self) -> None:
if not isinstance(self._mapping, dict):
raise WorkflowError("mapping.json должен быть объектом")
for mode, roles in ROLE_FIELDS.items():
mode_mapping = self._mapping.get(mode)
if not isinstance(mode_mapping, dict):
raise WorkflowError(f"В mapping.json отсутствует раздел {mode!r}")
template = self._templates[mode]
for role, (field_name, check) in roles.items():
node_id = mode_mapping.get(role)
if not isinstance(node_id, str) or node_id not in template:
raise WorkflowError(
f"{mode}: узел для роли {role!r} не найден в workflow"
)
node = template[node_id]
inputs = node.get("inputs")
if not isinstance(inputs, dict):
raise WorkflowError(f"{mode}: у узла {node_id} нет inputs")
if role == "latent":
if node.get("class_type") not in LATENT_CLASSES:
raise WorkflowError(
f"{mode}: узел latent должен создавать пустой латент"
)
if "width" not in inputs or "height" not in inputs:
raise WorkflowError(f"{mode}: у latent нет width/height")
continue
if field_name is None:
continue
if field_name not in inputs:
raise WorkflowError(
f"{mode}: у узла {node_id} нет поля {field_name!r} "
f"для роли {role!r}"
)
if check is not None and not check(inputs[field_name]):
raise WorkflowError(
f"{mode}: поле {field_name!r} у узла {node_id} "
f"имеет неожиданный тип"
)
def build_txt2img(self, *, prompt: str, negative: str, seed: int) -> dict[str, Any]:
workflow = copy.deepcopy(self._templates["txt2img"])
mapping = self._mapping["txt2img"]
workflow[mapping["positive"]]["inputs"]["text"] = prompt
workflow[mapping["negative"]]["inputs"]["text"] = negative
workflow[mapping["seed"]]["inputs"]["seed"] = seed
return workflow
def build_img2img(
self,
*,
prompt: str,
negative: str,
seed: int,
image_name: str,
width: int = 1024,
height: int = 1024,
) -> dict[str, Any]:
workflow = copy.deepcopy(self._templates["img2img"])
mapping = self._mapping["img2img"]
workflow[mapping["positive"]]["inputs"]["prompt"] = prompt
workflow[mapping["negative"]]["inputs"]["text"] = negative
workflow[mapping["seed"]]["inputs"]["noise_seed"] = seed
workflow[mapping["input_image"]]["inputs"]["image"] = image_name
latent = workflow[mapping["latent"]]["inputs"]
latent["width"] = width
latent["height"] = height
return workflow