Доработка управления моделями

This commit is contained in:
Shaman
2026-10-05 02:19:46 +03:00
parent f5ec608fc2
commit 0c5c0b423b
16 changed files with 629 additions and 252 deletions
+73 -36
View File
@@ -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)