ImageGen-Studio / mcp_tools /image_requests.py
BlueSkyXN's picture
Unify image references and guided UI/API workflows
ca9a89c verified
Raw History Blame Contribute Delete
9.01 kB
"""高层 API 图片参数兼容层;顺序与能力校验复用 UI 的执行计划。"""
import re
from PIL import Image
from core.execution_plan import (
MODE_MODEL_PK, MODE_MULTI_MODEL_GRID, MODE_MULTI_REFERENCE, MODE_SINGLE,
available_run_modes, build_execution_plan,
)
from core.reference_inputs import ImageInputError, REFERENCE_CHAIN_SPECS, SOURCE_IMAGE_KEYS, reference_choices
from core.runtime_config import CONFIG
NATIVE_CHAIN_ALIASES = {
"reference_edit": "reference_latent",
"mage_flow_reference_edit": "reference_image",
"joyai_reference_edit": "joyai_image",
"boogu_edit": "boogu_image_edit",
}
_IMAGE_KEY = re.compile(r"(?:图|img|image)([0-90-9]+)", re.IGNORECASE)
def normalize_image_request(params: dict) -> None:
"""只修改 handle_run 的副本。显式混用入口时拒绝,不猜优先级。"""
numbered = {}
for key in list(params):
match = _IMAGE_KEY.fullmatch(key) if isinstance(key, str) else None
if not match:
continue
index = int(match.group(1))
if index in numbered:
raise ImageInputError(f"图片编号 {index} 重复;图{index} / img{index} / image{index} 只能提供一个字段。")
numbered[index] = params.pop(key)
if numbered:
if "images" in params or params.get("image") is not None:
raise ImageInputError("编号字段、images 列表和 image 源图不能混用,请只保留一个入口。")
if sorted(numbered) != list(range(1, len(numbered) + 1)):
raise ImageInputError("图片编号必须从 1 开始且连续,不能缺少 img1 或跳号。")
params["images"] = [numbered[i] for i in range(1, len(numbered) + 1)]
if "images" in params:
images = params["images"]
if not isinstance(images, list) or not images:
raise ImageInputError("images 必须是非空、有序图片列表。")
if any(not isinstance(image, (str, Image.Image)) or (isinstance(image, str) and not image.strip()) for image in images):
raise ImageInputError("images 不允许空图片;删除空项后重新按顺序编号。")
if params.get("image") is not None:
raise ImageInputError("image 与 images 不能混用。")
if params["task_type"] == "reference":
params.setdefault("run_mode", MODE_MULTI_REFERENCE)
if params.get("image") is not None:
if not isinstance(params["image"], (str, Image.Image)):
raise ImageInputError("image 必须是图片 URL 或 Base64 Data URI。")
params["images"] = [params.pop("image")]
elif params["task_type"] == "txt2img" and params.get("image") is not None:
raise ImageInputError("txt2img 不使用 image 源图;参考图编辑请用 task_type=reference 和 images。")
if params.get("image") is not None and not isinstance(params["image"], (str, Image.Image)):
raise ImageInputError("image 必须是图片 URL 或 Base64 Data URI。")
for field in ("run_mode", "reference_role"):
if field in params and not isinstance(params[field], str):
raise ImageInputError(f"{field} 必须是字符串。")
if "extra_models" in params and (
not isinstance(params["extra_models"], list)
or not all(isinstance(model, str) for model in params["extra_models"])
):
raise ImageInputError("extra_models 必须是模型名称列表。")
if "use_model_defaults" in params and not isinstance(params["use_model_defaults"], bool):
raise ImageInputError("use_model_defaults 必须是 boolean。")
def validate_image_plan(params: dict) -> None:
"""不下载图片、不分配 GPU,提交队列前校验完整调用语义。"""
values = {
"task_type": params["task_type"], "model_display_name": params["model"],
"positive_prompt": params["prompt"], "negative_prompt": params.get("negative_prompt") or "",
"seed": params.get("seed", -1), "batch_size": params.get("batch_size", 1),
}
task = params["task_type"]
if task in SOURCE_IMAGE_KEYS and params.get("image") is not None:
values[SOURCE_IMAGE_KEYS[task]] = params["image"]
for item in params.get("chain", []):
chain = NATIVE_CHAIN_ALIASES.get(item["injector_type"], item["injector_type"])
if chain in REFERENCE_CHAIN_SPECS:
if params.get("images"):
raise ImageInputError("images 与原生参考 chain 不能混用;自动路由用 images,旧调用只用 chain。")
if item.get("image") is None:
raise ImageInputError(f"{chain} 缺少 image。")
if not isinstance(item["image"], (str, Image.Image)) or (isinstance(item["image"], str) and not item["image"].strip()):
raise ImageInputError(f"{chain}.image 必须是图片 URL 或 Base64 Data URI。")
values.setdefault(REFERENCE_CHAIN_SPECS[chain][0], []).append(item["image"])
mode = params.get("run_mode", MODE_SINGLE)
if mode not in {MODE_MODEL_PK, MODE_MULTI_MODEL_GRID} and params.get("extra_models"):
raise ImageInputError("extra_models 只用于 model_pk / multi_model_grid。")
if mode != MODE_MULTI_REFERENCE and params.get("reference_role", "auto") != "auto":
raise ImageInputError("reference_role 只用于 reference / multi_reference。")
if mode in {MODE_MODEL_PK, MODE_MULTI_MODEL_GRID} and (
params.get("chain") or params.get("vae_id") or params.get("pid") or params.get("pid_settings")
):
raise ImageInputError("模型对比仅使用基础模型,不支持 chain / VAE / PiD 覆盖。")
build_execution_plan(
values, mode=mode, images=params.get("images"),
extra_models=params.get("extra_models"), reference_role=params.get("reference_role", "auto"),
use_model_defaults=params.get("use_model_defaults", True),
)
def image_workflow_contract(model: str) -> dict:
"""在现有 discovery 上增量公开高层图片契约,不要求 AI 猜 injector。"""
roles = reference_choices(model)
return {
"reference_supported": bool(roles),
"reference_roles": {
role: {"chain": spec["chain"], "max_images": spec["max_images"]}
for role, spec in roles.items()
},
"run_modes_by_task": {
task: [mode for _, mode in available_run_modes(task, model)]
for task in ["txt2img", *SOURCE_IMAGE_KEYS, "reference"]
},
"parameters_schema": {
"type": "object",
"properties": {
"images": {"type": "array", "minItems": 1, "maxItems": CONFIG.max_multi_images,
"items": {"type": "string", "description": "Public HTTP(S) image URL or Base64 Data URI; not a local path."}},
"run_mode": {"type": "string", "enum": ["single", "model_pk", "multi_independent", "multi_model_grid", "multi_reference"],
"description": "Default single; task_type=reference defaults to multi_reference. Use run_modes_by_task."},
"reference_role": {"type": "string", "enum": list(roles) or ["auto"], "default": "auto"},
"extra_models": {"type": "array", "items": {"type": "string"}, "description": "Only for model_pk / multi_model_grid; current model is included automatically."},
"use_model_defaults": {"type": "boolean", "default": True},
},
"patternProperties": {r"^(图|img|image)[1-9][0-9]*$": {"type": "string", "description": "Alternative to images; indices must start at 1, be contiguous and unique."}},
},
"rules": [
"Use task_type=reference with images, prompt, width, height for instruction editing / fusion; no native chain needed. Model must support reference.",
"images order is 1-based: 图1 = img1 = image1. Numbered prompt references become image 1; quoted/backticked text is literal. No auto translation or prompt expansion.",
"Do not mix images, numbered fields, image, or native reference chain. Legacy txt2img + chain and single-image image calls remain supported.",
"multi_independent / multi_model_grid use one shared prompt per image. Say 当前图片 / the current image; cross-image numbered references are rejected.",
"Leave steps/cfg/sampler/scheduler omitted to use model defaults. Negative prompts depend on model/CFG and may be unused at CFG=1.",
"Prefer async_execution=true; poll the same task_id with get_task_status until completed/failed; do not resubmit on a polling timeout.",
"Read result.image_references, effective_prompt, captions and jobs. Existing result.images remains a flat URL list; partial_success signals mixed results.",
],
"limits": {"max_images": CONFIG.max_multi_images, "max_pk_models": CONFIG.max_pk_models,
"max_jobs": CONFIG.max_plan_jobs, "max_outputs": CONFIG.max_plan_outputs},
}