Spaces:
Running on Zero
Running on Zero
Download mcp_tools/run.py from BlueSkyXN/ImageGen-Studio: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/BlueSkyXN/ImageGen-Studio/resolve/main/mcp_tools/run.py
- Command line
-
hf download hf://spaces/BlueSkyXN/ImageGen-Studio/mcp_tools/run.py
-
curl -L -o run.py https://huggingface.co/spaces/BlueSkyXN/ImageGen-Studio/resolve/main/mcp_tools/run.py
11.7 kB
| """ | |
| MCP Tool: run | |
| Unified image generation task submission and execution interface. | |
| """ | |
| import time | |
| import uuid | |
| from core.task_scheduler import QueueFullError, submit_background | |
| from core.runtime_config import CONFIG | |
| from .common import ( | |
| _load_yaml, | |
| _MODEL_LIST_PATH, | |
| _CHAIN_FEATURES_PATH, | |
| _TASK_DEFINITIONS, | |
| _TASKS_DB, | |
| _TASKS_LOCK, | |
| _get_task_snapshot, | |
| _execute_imagegen_pipeline, | |
| _get_model_feature_context, | |
| _canonicalize_chain_type, | |
| _resolve_feature_name, | |
| ) | |
| from .error_schema import make_error, make_validation_error, make_not_found_error | |
| from .image_requests import normalize_image_request, validate_image_plan | |
| from core.reference_inputs import ImageInputError | |
| def handle_run(params: dict) -> dict: | |
| """Unified image generation task execution interface.""" | |
| if not isinstance(params, dict): | |
| return make_validation_error("Request params must be an object.") | |
| params = params.copy() | |
| if isinstance(params.get("chain"), list): | |
| params["chain"] = [ | |
| item.copy() if isinstance(item, dict) else item | |
| for item in params["chain"] | |
| ] | |
| missing = [] | |
| for req_field in ["task_type", "model", "prompt"]: | |
| if req_field not in params or not params[req_field]: | |
| missing.append(req_field) | |
| if missing: | |
| return make_validation_error( | |
| f"Missing required parameter(s): {', '.join(missing)}", | |
| missing_fields=missing, | |
| ) | |
| task_type = params["task_type"] | |
| if ( | |
| not isinstance(task_type, str) | |
| or not isinstance(params["model"], str) | |
| or not isinstance(params["prompt"], str) | |
| or not task_type.strip() | |
| or not params["model"].strip() | |
| or not params["prompt"].strip() | |
| ): | |
| return make_validation_error( | |
| "task_type, model, and prompt must be strings.", | |
| invalid_fields={ | |
| key: "Expected a non-empty string" | |
| for key in ("task_type", "model", "prompt") | |
| if not isinstance(params.get(key), str) or not params.get(key, "").strip() | |
| }, | |
| ) | |
| task_type = task_type.strip() | |
| params["task_type"] = task_type | |
| params["model"] = params["model"].strip() | |
| valid_tasks = [t["task_type"] for t in _TASK_DEFINITIONS] | |
| if task_type not in valid_tasks: | |
| return make_validation_error( | |
| f"Invalid task_type '{task_type}'. Must be one of {valid_tasks}.", | |
| invalid_fields={"task_type": f"Must be in {valid_tasks}"}, | |
| ) | |
| model_list = _load_yaml(_MODEL_LIST_PATH) | |
| checkpoints = model_list.get("Checkpoint", {}) | |
| all_models = set() | |
| for arch_name, arch_data in checkpoints.items(): | |
| if isinstance(arch_data, dict): | |
| for m in arch_data.get("models", []): | |
| all_models.add(m.get("display_name")) | |
| if params["model"] not in all_models: | |
| return make_not_found_error("model", params["model"]) | |
| try: | |
| normalize_image_request(params) | |
| except ImageInputError as exc: | |
| return make_validation_error(str(exc), invalid_fields={"images": str(exc)}) | |
| task_requirements = { | |
| "txt2img": ("width", "height"), | |
| "reference": ("width", "height", "images"), | |
| "img2img": ("image",), | |
| "inpaint": ("image",), | |
| "outpaint": ("image", "pad_left", "pad_right", "pad_top", "pad_bottom"), | |
| "hires_fix": ("image", "upscale_by"), | |
| } | |
| missing_task_fields = [ | |
| field | |
| for field in task_requirements.get(task_type, ()) | |
| if not (field == "image" and params.get("images")) | |
| if field not in params or params[field] is None or params[field] == "" | |
| ] | |
| if missing_task_fields: | |
| return make_validation_error( | |
| f"Missing required parameter(s) for {task_type}: {', '.join(missing_task_fields)}", | |
| missing_fields=missing_task_fields, | |
| ) | |
| invalid_fields = {} | |
| chain_features = _load_yaml(_CHAIN_FEATURES_PATH) | |
| def _number(name, *, integer=False, minimum=None, maximum=None): | |
| if name not in params: | |
| return | |
| raw_value = params[name] | |
| if raw_value is None or (isinstance(raw_value, str) and not raw_value.strip()): | |
| if name in task_requirements.get(task_type, ()): | |
| invalid_fields[name] = "Expected a non-empty number" | |
| else: | |
| # 可选数值的空值等同于未提供,统一沿用执行层的任务 / 模型默认值。 | |
| params.pop(name) | |
| return | |
| if isinstance(raw_value, bool): | |
| invalid_fields[name] = "Expected a number" if not integer else "Expected an integer" | |
| return | |
| try: | |
| numeric_value = float(raw_value) | |
| except (TypeError, ValueError): | |
| invalid_fields[name] = "Expected a number" if not integer else "Expected an integer" | |
| return | |
| if numeric_value != numeric_value or numeric_value in (float("inf"), float("-inf")): | |
| invalid_fields[name] = "Must be finite" | |
| return | |
| if integer and not numeric_value.is_integer(): | |
| invalid_fields[name] = "Expected an integer" | |
| return | |
| value = int(numeric_value) if integer else numeric_value | |
| if minimum is not None and value < minimum: | |
| invalid_fields[name] = f"Must be >= {minimum}" | |
| elif maximum is not None and value > maximum: | |
| invalid_fields[name] = f"Must be <= {maximum}" | |
| else: | |
| params[name] = value | |
| for field in ("width", "height"): | |
| if task_type == "txt2img" or field in params: | |
| _number(field, integer=True, minimum=1, maximum=8192) | |
| for field in ("pad_left", "pad_right", "pad_top", "pad_bottom"): | |
| _number(field, integer=True, minimum=0, maximum=4096) | |
| _number("upscale_by", minimum=0.01, maximum=4.0) | |
| _number("denoise", minimum=0.0, maximum=1.0) | |
| _number("steps", integer=True, minimum=1, maximum=100) | |
| _number("cfg", minimum=0.0, maximum=100.0) | |
| _number("batch_size", integer=True, minimum=1, maximum=CONFIG.max_batch_size) | |
| _number("seed", integer=True, minimum=-1) | |
| for field in ("sampler", "scheduler", "negative_prompt"): | |
| if field in params and params[field] is not None and not isinstance(params[field], str): | |
| invalid_fields[field] = "Expected a string" | |
| if "async_execution" in params: | |
| async_execution = params["async_execution"] | |
| if isinstance(async_execution, str): | |
| normalized = async_execution.strip().casefold() | |
| if normalized in {"true", "1"}: | |
| params["async_execution"] = True | |
| elif normalized in {"false", "0"}: | |
| params["async_execution"] = False | |
| else: | |
| invalid_fields["async_execution"] = "Expected true/false or 1/0" | |
| elif not isinstance(async_execution, bool): | |
| invalid_fields["async_execution"] = "Expected a boolean" | |
| if "chain" in params: | |
| chain = params["chain"] | |
| if not isinstance(chain, list): | |
| invalid_fields["chain"] = "Expected a list of objects" | |
| else: | |
| context = _get_model_feature_context(params["model"]) | |
| for index, item in enumerate(chain): | |
| field_name = f"chain[{index}]" | |
| if not isinstance(item, dict): | |
| invalid_fields[field_name] = "Expected an object" | |
| continue | |
| injector_type = item.get("injector_type") | |
| if not isinstance(injector_type, str) or not injector_type.strip(): | |
| invalid_fields[field_name] = "injector_type must be a non-empty string" | |
| continue | |
| injector_type = injector_type.strip() | |
| injector_type = _canonicalize_chain_type(injector_type) | |
| item["injector_type"] = injector_type | |
| feature_name = _resolve_feature_name(injector_type) | |
| if feature_name is None: | |
| return make_validation_error( | |
| f"Unknown chain/injector type '{injector_type}'.", | |
| invalid_fields={field_name: "Unknown injector_type"}, | |
| ) | |
| feature_data = chain_features.get(feature_name, {}) | |
| aliases = feature_data.get("chains", feature_name) | |
| aliases = aliases if isinstance(aliases, list) else [aliases] | |
| enabled = context and context["enabled_chains"] | |
| supports = context and context["supports_chain"] | |
| if not enabled or not any(alias in enabled for alias in [feature_name, *aliases]): | |
| return make_error( | |
| "FEATURE_NOT_SUPPORTED", | |
| f"Model '{params['model']}' does not support chain '{injector_type}'.", | |
| {"model": params["model"], "chain": injector_type}, | |
| ) | |
| if supports and not any(supports(alias) for alias in [injector_type, feature_name, *aliases]): | |
| return make_error( | |
| "FEATURE_NOT_SUPPORTED", | |
| f"Model '{params['model']}' does not support chain '{injector_type}'.", | |
| {"model": params["model"], "chain": injector_type}, | |
| ) | |
| if invalid_fields: | |
| return make_validation_error("Request contains invalid parameters.", invalid_fields=invalid_fields) | |
| try: | |
| validate_image_plan(params) | |
| except ImageInputError as exc: | |
| return make_validation_error(str(exc), invalid_fields={"image_plan": str(exc)}) | |
| batch_size = params.get("batch_size", 1) | |
| if not 1 <= batch_size <= CONFIG.max_batch_size: | |
| return make_validation_error( | |
| f"batch_size must be between 1 and {CONFIG.max_batch_size}.", | |
| invalid_fields={"batch_size": f"Expected 1..{CONFIG.max_batch_size}"}, | |
| ) | |
| with _TASKS_LOCK: | |
| if len(_TASKS_DB) >= CONFIG.mcp_task_retention: | |
| finished = sorted( | |
| ( | |
| (old_task_id, task) | |
| for old_task_id, task in _TASKS_DB.items() | |
| if task.get("status") in {"completed", "failed"} | |
| ), | |
| key=lambda pair: pair[1].get("completed_at", pair[1].get("failed_at", 0)), | |
| ) | |
| remove_count = max(1, len(_TASKS_DB) - CONFIG.mcp_task_retention + 1) | |
| for old_task_id, _ in finished[:remove_count]: | |
| _TASKS_DB.pop(old_task_id, None) | |
| if len(_TASKS_DB) >= CONFIG.mcp_task_retention: | |
| return make_error( | |
| "QUEUE_FULL", | |
| "任务记录已满且当前任务均未结束,请稍后再试。", | |
| ) | |
| task_id = f"img_task_{uuid.uuid4().hex[:10]}" | |
| created_at = int(time.time()) | |
| _TASKS_DB[task_id] = { | |
| "task_id": task_id, | |
| "status": "queued", | |
| "progress": 0, | |
| "created_at": created_at, | |
| } | |
| async_exec = params.get("async_execution", False) | |
| if async_exec: | |
| try: | |
| submit_background(_execute_imagegen_pipeline, task_id, params) | |
| except QueueFullError as exc: | |
| with _TASKS_LOCK: | |
| _TASKS_DB.pop(task_id, None) | |
| return make_error("QUEUE_FULL", str(exc)) | |
| return { | |
| "status": "queued", | |
| "task_id": task_id, | |
| "poll_interval_ms": 2000, | |
| "message": "Task queued successfully. Poll get_task_status for results.", | |
| } | |
| else: | |
| _execute_imagegen_pipeline(task_id, params) | |
| return _get_task_snapshot(task_id) | |