Доработка управления моделями
This commit is contained in:
+73
-36
@@ -1,5 +1,12 @@
|
||||
"""Build a concrete workflow graph from a template + request parameters."""
|
||||
"""Build a concrete workflow graph from a template + request values.
|
||||
|
||||
Values are injected through placeholder tokens written directly inside the
|
||||
workflow graph (e.g. "{prompt}", "{seed}"), so the graph itself declares which
|
||||
node input receives which request field. The set of recognised tokens is fixed
|
||||
and owned by the adapter, not by the model.
|
||||
"""
|
||||
import copy
|
||||
import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
@@ -7,6 +14,21 @@ from .errors import InvalidRequest
|
||||
|
||||
SEED_MAX = 2**63 - 1
|
||||
|
||||
# Fixed token vocabulary owned by the adapter. A model's workflow may only use
|
||||
# these tokens; anything else is rejected. Types drive typed vs string injection.
|
||||
TOKEN_TYPES: dict[str, str] = {
|
||||
"prompt": "str",
|
||||
"negative": "str",
|
||||
"seed": "int",
|
||||
"width": "int",
|
||||
"height": "int",
|
||||
"image": "str",
|
||||
}
|
||||
KNOWN_TOKENS = frozenset(TOKEN_TYPES)
|
||||
|
||||
_TOKEN = re.compile(r"\{(\w+)\}")
|
||||
_WHOLE = re.compile(r"^\{(\w+)\}$")
|
||||
|
||||
|
||||
def parse_size(size: str) -> tuple[int, int]:
|
||||
"""Parse OpenAI `WxH` size strings (e.g. '1024x1024')."""
|
||||
@@ -28,40 +50,55 @@ def new_seed(seed: int | None) -> int:
|
||||
return seed
|
||||
|
||||
|
||||
def build_workflow(
|
||||
template: dict[str, Any],
|
||||
mapping: dict[str, dict[str, str]],
|
||||
*,
|
||||
prompt: str,
|
||||
seed: int,
|
||||
size: str | None = None,
|
||||
image_name: str | None = None,
|
||||
negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Deep-copy the template and inject request values per the node mapping."""
|
||||
def extract_tokens(graph: dict[str, Any]) -> tuple[set[str], list[str]]:
|
||||
"""Token names referenced by the graph.
|
||||
|
||||
Returns (known_tokens, unknown_token_errors). A token is any `{name}` inside
|
||||
a string node input. Unknown names are reported so startup can reject the
|
||||
workflow before it ever reaches a request.
|
||||
"""
|
||||
declared: set[str] = set()
|
||||
unknown: set[str] = set()
|
||||
for node_id, node in graph.items():
|
||||
for field, value in node.get("inputs", {}).items():
|
||||
if isinstance(value, str):
|
||||
for name in _TOKEN.findall(value):
|
||||
if name in KNOWN_TOKENS:
|
||||
declared.add(name)
|
||||
else:
|
||||
unknown.add(f"node {node_id}: input '{field}' uses unknown token '{{{name}}}'")
|
||||
return declared, sorted(unknown)
|
||||
|
||||
|
||||
def build_workflow(template: dict[str, Any], values: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Deep-copy the template and substitute placeholder tokens with values.
|
||||
|
||||
Injection is type-preserving: a value that is exactly one token (e.g.
|
||||
"{seed}") is replaced by the typed object (int for seed/width/height, str
|
||||
otherwise); a token embedded in surrounding text is string-interpolated.
|
||||
A referenced token that is unrecognized or missing from ``values`` rejects
|
||||
the build, so a malformed graph never reaches ComfyUI.
|
||||
"""
|
||||
graph = copy.deepcopy(template)
|
||||
|
||||
def put(key: str, value: Any) -> None:
|
||||
spec = mapping.get(key)
|
||||
if spec is None:
|
||||
return
|
||||
node = graph.get(spec["node"])
|
||||
if node is None:
|
||||
raise InvalidRequest(f"mapping key '{key}' points to missing node '{spec['node']}'")
|
||||
node["inputs"][spec["field"]] = value
|
||||
|
||||
put("prompt", prompt)
|
||||
put("negative", negative)
|
||||
put("seed", seed)
|
||||
if image_name is not None:
|
||||
put("image", image_name)
|
||||
if size is not None:
|
||||
spec = mapping.get("size")
|
||||
if spec is not None:
|
||||
width, height = parse_size(size)
|
||||
node = graph.get(spec["node"])
|
||||
if node is None:
|
||||
raise InvalidRequest(f"mapping key 'size' points to missing node '{spec['node']}'")
|
||||
node["inputs"][spec["width_field"]] = width
|
||||
node["inputs"][spec["height_field"]] = height
|
||||
for node in graph.values():
|
||||
inputs = node.get("inputs", {})
|
||||
for field, value in inputs.items():
|
||||
if isinstance(value, str):
|
||||
inputs[field] = _substitute(value, values)
|
||||
return graph
|
||||
|
||||
|
||||
def _substitute(value: str, values: dict[str, Any]) -> Any:
|
||||
for name in _TOKEN.findall(value):
|
||||
if name not in KNOWN_TOKENS:
|
||||
raise InvalidRequest(f"workflow uses unrecognized token '{{{name}}}'")
|
||||
if name not in values:
|
||||
raise InvalidRequest(f"no value supplied for token '{{{name}}}'")
|
||||
whole = _WHOLE.fullmatch(value)
|
||||
if whole:
|
||||
return _coerce(whole.group(1), values[whole.group(1)])
|
||||
return _TOKEN.sub(lambda m: str(values[m.group(1)]), value)
|
||||
|
||||
|
||||
def _coerce(name: str, value: Any) -> Any:
|
||||
return int(value) if TOKEN_TYPES[name] == "int" else str(value)
|
||||
|
||||
Reference in New Issue
Block a user