aimusic / ComfyUI /custom_nodes /koolook /koolook_setup_runner.py
vidfom's picture
Upload folder using huggingface_hub (part 8)
0c85e62 verified
Raw
History Blame Contribute Delete
37.4 kB
"""Execution boundary for published Koolook workflow setups."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
try:
from .koolook_setups import PublishedSetupRegistry
except ImportError: # pragma: no cover - standalone test/import context
from koolook_setups import PublishedSetupRegistry
class SetupRunError(Exception):
"""API-facing setup runner error with stable HTTP status semantics."""
def __init__(self, status_code: int, errors: list[str]) -> None:
super().__init__("; ".join(errors))
self.status_code = status_code
self.errors = errors
@dataclass(frozen=True)
class RunRecord:
run_id: str
setup_id: str
prompt_id: str
status: str
inputs: dict[str, Any]
class InMemorySetupRunStore:
"""Small run-id mapping store for a running ComfyUI process."""
def __init__(self) -> None:
self._next_id = 1
self._records: dict[str, RunRecord] = {}
def create(self, *, setup_id: str, prompt_id: str, inputs: dict[str, Any] | None = None) -> RunRecord:
run_id = f"run-{self._next_id:06d}"
self._next_id += 1
record = RunRecord(
run_id=run_id,
setup_id=setup_id,
prompt_id=prompt_id,
status="queued",
inputs=deepcopy(inputs) if isinstance(inputs, dict) else {},
)
self._records[run_id] = record
return record
def get(self, run_id: str) -> RunRecord | None:
return self._records.get(run_id)
class AiohttpComfyClient:
"""HTTP adapter for the managed ComfyUI server."""
def __init__(self, base_url: str) -> None:
self._base_url = base_url.rstrip("/")
async def queue_prompt(self, prompt: dict) -> dict:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(f"{self._base_url}/prompt", json={"prompt": prompt}) as response:
try:
payload = await response.json()
except Exception:
payload = {"error": await response.text()}
if response.status >= 400:
detail = payload.get("error") if isinstance(payload, dict) else payload
raise SetupRunError(
502,
[f"ComfyUI queue request failed with HTTP {response.status}: {detail}"],
)
return payload if isinstance(payload, dict) else {}
async def get_history(self, prompt_id: str) -> dict:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"{self._base_url}/history/{prompt_id}") as response:
try:
payload = await response.json()
except Exception:
payload = {"error": await response.text()}
if response.status >= 400:
detail = payload.get("error") if isinstance(payload, dict) else payload
raise SetupRunError(
502,
[f"ComfyUI history request failed with HTTP {response.status}: {detail}"],
)
return payload if isinstance(payload, dict) else {}
async def get_queue(self) -> dict:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"{self._base_url}/queue") as response:
try:
payload = await response.json()
except Exception:
payload = {"error": await response.text()}
if response.status >= 400:
detail = payload.get("error") if isinstance(payload, dict) else payload
raise SetupRunError(
502,
[f"ComfyUI queue status request failed with HTTP {response.status}: {detail}"],
)
return payload if isinstance(payload, dict) else {}
class PublishedSetupRunner:
"""Runs callable published setups without exposing ComfyUI internals."""
def __init__(
self,
registry: PublishedSetupRegistry,
comfy_client: Any,
run_store: InMemorySetupRunStore | None = None,
) -> None:
self._registry = registry
self._comfy_client = comfy_client
self._run_store = run_store or InMemorySetupRunStore()
async def runSetup(self, setup_id: str, inputs: dict[str, Any]) -> dict[str, Any]:
setup = self._registry.getSetup(setup_id)
if setup is None:
raise SetupRunError(404, [f"Published setup '{setup_id}' not found."])
if not isinstance(inputs, dict):
raise SetupRunError(400, ["inputs must be a JSON object"])
api_prompt = setup.get("apiPrompt")
if not isinstance(api_prompt, dict) or setup.get("validation", {}).get("status") != "valid":
raise SetupRunError(400, [f"Published setup '{setup_id}' is not callable."])
prompt = deepcopy(api_prompt)
input_fields = _declared_input_fields(setup)
errors = _validate_run_inputs(input_fields, inputs)
errors.extend(_validate_execution_map_inputs(setup, inputs))
if errors:
raise SetupRunError(400, errors)
for key, value in inputs.items():
field = input_fields[key]
target = field["target"]
prompt[str(target["node"])]["inputs"][str(target["input"])] = _prompt_value_for_field(field, value)
mapped_prompt = _prune_prompt_for_execution_map(setup, prompt, inputs)
prompt = (
mapped_prompt
if mapped_prompt is not None
else _prune_prompt_for_selected_app_results(setup, prompt, inputs)
)
try:
queued = await self._comfy_client.queue_prompt(prompt)
except SetupRunError:
raise
except Exception as exc:
raise SetupRunError(502, [f"ComfyUI queue request failed: {exc}"]) from exc
prompt_id = queued.get("prompt_id") if isinstance(queued, dict) else None
if not isinstance(prompt_id, str) or not prompt_id:
raise SetupRunError(502, ["ComfyUI queue response did not include prompt_id"])
record = self._run_store.create(setup_id=setup_id, prompt_id=prompt_id, inputs=inputs)
return {"runId": record.run_id, "promptId": record.prompt_id, "status": record.status}
async def getRun(self, run_id: str) -> dict[str, Any]:
record = self._run_store.get(run_id)
if record is None:
raise SetupRunError(404, [f"Koolook run '{run_id}' not found."])
setup = self._registry.getSetup(record.setup_id)
if setup is None:
raise SetupRunError(404, [f"Published setup '{record.setup_id}' not found."])
try:
history = await self._comfy_client.get_history(record.prompt_id)
except SetupRunError:
raise
except Exception as exc:
raise SetupRunError(502, [f"ComfyUI history request failed: {exc}"]) from exc
history_entry = history.get(record.prompt_id) if isinstance(history, dict) else None
if isinstance(history_entry, dict):
status = _status_from_history(history_entry)
return {
"runId": record.run_id,
"setupId": record.setup_id,
"promptId": record.prompt_id,
"status": status,
"comfyStatus": history_entry.get("status", {}),
"outputs": _summarize_outputs(
setup,
history_entry.get("outputs", {}),
record.inputs,
_history_prompt(history_entry),
),
}
try:
queue = await self._comfy_client.get_queue()
except SetupRunError:
raise
except Exception as exc:
raise SetupRunError(502, [f"ComfyUI queue status request failed: {exc}"]) from exc
return {
"runId": record.run_id,
"setupId": record.setup_id,
"promptId": record.prompt_id,
"status": _status_from_queue(record.prompt_id, queue),
"outputs": [],
}
def _declared_input_fields(setup: dict[str, Any]) -> dict[str, dict[str, Any]]:
fields: dict[str, dict[str, Any]] = {}
for field in setup.get("inputContract", {}).get("inputs", []):
if not isinstance(field, dict):
continue
key = field.get("key")
target = field.get("target")
if isinstance(key, str) and key and isinstance(target, dict):
fields[key] = field
app = setup.get("setupSurface", {}).get("app", {})
if isinstance(app, dict):
for field in app.get("inputs", []):
if not isinstance(field, dict):
continue
key = field.get("key")
target = field.get("target")
if isinstance(key, str) and key and isinstance(target, dict):
fields.setdefault(key, field)
for field in app.get("outputs", []):
if not isinstance(field, dict):
continue
key = field.get("key")
target = field.get("target")
if isinstance(key, str) and key and isinstance(target, dict):
fields.setdefault(key, field)
switch = app.get("switch")
if isinstance(switch, dict):
key = switch.get("key")
target = switch.get("target")
if isinstance(key, str) and key and isinstance(target, dict):
fields.setdefault(key, switch)
return fields
def _validate_run_inputs(input_fields: dict[str, dict[str, Any]], inputs: dict[str, Any]) -> list[str]:
errors: list[str] = []
for key in inputs:
if key not in input_fields:
errors.append(f"input '{key}' is not declared by this setup")
continue
field = input_fields[key]
if field.get("key") == "switch" and _switch_option_value(field, inputs[key]) is None:
errors.append(_switch_value_error(key, field))
for key, field in input_fields.items():
if field.get("required") is True and key not in inputs:
errors.append(f"required input '{key}' is missing")
return errors
def _prompt_value_for_field(field: dict[str, Any], value: Any) -> Any:
if field.get("key") != "switch":
return value
selected_value = _switch_option_value(field, value)
for option in field.get("options", []):
if not isinstance(option, dict):
continue
if option.get("value") == selected_value and isinstance(option.get("label"), str):
return option["label"]
return value
def _switch_option_value(switch: dict[str, Any], selected: Any) -> int | None:
if isinstance(selected, bool):
return None
for option in switch.get("options", []):
if not isinstance(option, dict):
continue
value = option.get("value")
if isinstance(value, bool) or not isinstance(value, int):
continue
if selected == value:
return value
if isinstance(selected, str) and (selected == str(value) or selected == option.get("label")):
return value
return None
def _switch_value_error(key: str, switch: dict[str, Any]) -> str:
choices: list[str] = []
for option in switch.get("options", []):
if not isinstance(option, dict):
continue
value = option.get("value")
label = option.get("label")
if isinstance(value, bool) or not isinstance(value, int):
continue
choices.append(f"{value} ({label})" if isinstance(label, str) and label else str(value))
if choices:
return f"input '{key}' must be one of: {', '.join(choices)}"
return f"input '{key}' has no valid switch options"
def _status_from_history(history_entry: dict[str, Any]) -> str:
status = history_entry.get("status")
if not isinstance(status, dict):
return "succeeded"
status_text = str(status.get("status_str", "")).lower()
if status.get("completed") is True and status_text not in {"error", "failed", "failure"}:
return "succeeded"
return "failed"
def _status_from_queue(prompt_id: str, queue: Any) -> str:
if not isinstance(queue, dict):
return "lost"
if _queue_contains_prompt(queue.get("queue_running", []), prompt_id):
return "running"
if _queue_contains_prompt(queue.get("queue_pending", []), prompt_id):
return "queued"
return "lost"
def _queue_contains_prompt(entries: Any, prompt_id: str) -> bool:
if not isinstance(entries, list):
return False
for entry in entries:
if isinstance(entry, dict) and entry.get("prompt_id") == prompt_id:
return True
if isinstance(entry, list) and prompt_id in entry:
return True
return False
def _summarize_outputs(
setup: dict[str, Any],
raw_outputs: Any,
run_inputs: dict[str, Any] | None = None,
history_prompt: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
output_items = _flatten_history_outputs(raw_outputs)
summaries = []
for output in setup.get("outputContract", {}).get("outputs", []):
if not isinstance(output, dict):
continue
summaries.append(
{
"key": output.get("key", ""),
"label": output.get("label", output.get("key", "")),
"type": output.get("type", ""),
"items": [
item
for item in output_items
if _history_output_matches_contract(item, str(output.get("type", "")))
],
}
)
summaries.extend(_summarize_app_surface_outputs(setup, output_items, run_inputs or {}, history_prompt))
return summaries
def _summarize_app_surface_outputs(
setup: dict[str, Any],
output_items: list[dict[str, Any]],
run_inputs: dict[str, Any],
history_prompt: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
app = setup.get("setupSurface", {}).get("app", {})
if not isinstance(app, dict):
return []
summaries: list[dict[str, Any]] = []
for field_type, fields in (("output", app.get("outputs")), ("result", app.get("results"))):
if not isinstance(fields, list):
continue
for field in fields:
if not isinstance(field, dict):
continue
target = field.get("target")
target_node = str(target.get("node")) if isinstance(target, dict) else ""
items = [
item
for item in output_items
if target_node and item.get("nodeId") == target_node
]
if field_type == "result" and not items:
items = _selected_switch_result_items(setup, field, output_items, run_inputs)
if field_type == "result" and not items and history_prompt is not None:
items = _execution_map_writer_result_items(setup, history_prompt, run_inputs)
summary = {
"key": field.get("key", ""),
"label": field.get("label", field.get("key", "")),
"type": field_type,
"visible": field.get("visible", True),
"target": target if isinstance(target, dict) else {},
"default": field.get("default"),
"items": items,
}
summaries.append(summary)
return summaries
def _history_prompt(history_entry: dict[str, Any]) -> dict[str, Any] | None:
prompt_record = history_entry.get("prompt")
if (
isinstance(prompt_record, list)
and len(prompt_record) >= 3
and isinstance(prompt_record[2], dict)
):
return prompt_record[2]
return None
def _execution_map_writer_result_items(
setup: dict[str, Any],
prompt: dict[str, Any],
run_inputs: dict[str, Any],
) -> list[dict[str, Any]]:
writer_nodes = _selected_execution_map_writer_nodes(setup, run_inputs)
items: list[dict[str, Any]] = []
for writer_node in writer_nodes:
node = prompt.get(writer_node)
if not isinstance(node, dict):
continue
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
value = _resolve_prompt_value(prompt, inputs.get("filepath"))
if isinstance(value, str) and value:
items.append({"nodeId": writer_node, "kind": "text", "value": value})
return items
def _selected_execution_map_writer_nodes(
setup: dict[str, Any],
run_inputs: dict[str, Any],
) -> list[str]:
execution_map = setup.get("executionMap")
if not isinstance(execution_map, dict) or execution_map.get("version") != 1:
return []
routers = execution_map.get("routers")
if not isinstance(routers, list):
return []
out: list[str] = []
for router in routers:
if not isinstance(router, dict):
continue
selected_value = _execution_map_selected_value(setup, router, run_inputs)
branches = router.get("branches")
branch = branches.get(str(selected_value)) if isinstance(branches, dict) else None
writer_nodes = branch.get("writerNodes") if isinstance(branch, dict) else None
if isinstance(writer_nodes, list):
out.extend(str(node_id) for node_id in writer_nodes)
return list(dict.fromkeys(out))
def _selected_switch_result_items(
setup: dict[str, Any],
result_field: dict[str, Any],
output_items: list[dict[str, Any]],
run_inputs: dict[str, Any],
) -> list[dict[str, Any]]:
selected_branch = _selected_result_branch_ref(setup, result_field, run_inputs)
if selected_branch is None:
return []
selected_node = str(selected_branch[0])
return [item for item in output_items if item.get("nodeId") == selected_node]
def _prune_prompt_for_execution_map(
setup: dict[str, Any],
prompt: dict[str, Any],
run_inputs: dict[str, Any],
) -> dict[str, Any] | None:
execution_map = setup.get("executionMap")
if not isinstance(execution_map, dict) or execution_map.get("version") != 1:
return None
routers = execution_map.get("routers")
if not isinstance(routers, list):
return None
roots: set[str] = set()
for router in routers:
if not isinstance(router, dict):
continue
router_node = str(router.get("node", ""))
selected_value = _execution_map_selected_value(setup, router, run_inputs)
if selected_value is None:
continue
branches = router.get("branches")
branch = branches.get(str(selected_value)) if isinstance(branches, dict) else None
if not isinstance(branch, dict):
continue
writer_nodes = branch.get("writerNodes")
if isinstance(writer_nodes, list):
roots.update(str(node_id) for node_id in writer_nodes if str(node_id) in prompt)
if not writer_nodes and router_node in prompt:
roots.add(router_node)
if not roots:
return None
return _prune_prompt_from_roots(
prompt,
roots,
_selected_app_switches(setup, prompt, run_inputs),
)
def _execution_map_selected_value(
setup: dict[str, Any],
router: dict[str, Any],
run_inputs: dict[str, Any],
) -> int | None:
key = router.get("switchKey")
app = setup.get("setupSurface", {}).get("app", {})
switch = app.get("switch") if isinstance(app, dict) else None
if isinstance(switch, dict) and switch.get("key") == key:
return _selected_switch_value(switch, run_inputs)
selected = run_inputs.get(key) if isinstance(key, str) else None
if isinstance(selected, bool):
return None
if isinstance(selected, int):
return selected
if isinstance(selected, str) and selected.isdigit():
return int(selected)
return None
def _validate_execution_map_inputs(setup: dict[str, Any], run_inputs: dict[str, Any]) -> list[str]:
execution_map = setup.get("executionMap")
if not isinstance(execution_map, dict) or execution_map.get("version") != 1:
return []
routers = execution_map.get("routers")
if not isinstance(routers, list):
return []
api_prompt = setup.get("apiPrompt")
prompt_node_ids = set(api_prompt) if isinstance(api_prompt, dict) else set()
app = setup.get("setupSurface", {}).get("app", {})
switch = app.get("switch") if isinstance(app, dict) else None
errors: list[str] = []
for router in routers:
if not isinstance(router, dict):
continue
selected_value = _execution_map_selected_value(setup, router, run_inputs)
key = router.get("switchKey")
if selected_value is None:
if isinstance(switch, dict) and switch.get("key") == key and isinstance(key, str):
if key not in run_inputs:
errors.append(_switch_value_error(key, switch))
elif isinstance(key, str):
errors.append(f"input '{key}' must select a valid execution branch")
continue
branches = router.get("branches")
if not isinstance(branches, dict) or str(selected_value) not in branches:
errors.append(f"input '{key}' selects branch {selected_value}, but this setup has no execution branch for it")
continue
branch = branches.get(str(selected_value))
writer_nodes = branch.get("writerNodes") if isinstance(branch, dict) else None
if isinstance(writer_nodes, list) and writer_nodes:
missing_writer_nodes = [
str(node_id)
for node_id in writer_nodes
if str(node_id) not in prompt_node_ids
]
if missing_writer_nodes:
errors.append(
f"execution map branch {selected_value} for switch '{key}' references "
f"writer node(s) not present in the prompt: {', '.join(missing_writer_nodes)}"
)
elif not writer_nodes and str(router.get("node", "")) not in prompt_node_ids:
errors.append(
f"execution map branch {selected_value} for switch '{key}' has no writer nodes "
"and its router node is not present in the prompt"
)
return errors
def _prune_prompt_for_selected_app_results(
setup: dict[str, Any],
prompt: dict[str, Any],
run_inputs: dict[str, Any],
) -> dict[str, Any]:
result_switches = _selected_result_switches(setup, prompt, run_inputs)
if not result_switches:
return prompt
selected_switches = _selected_app_switches(setup, prompt, run_inputs)
roots = {
node_id
for node_id, _selected_value in result_switches.values()
if node_id in prompt
}
for field in _app_result_fields(setup):
target = field.get("target")
if isinstance(target, dict) and str(target.get("node")) in prompt:
roots.add(str(target["node"]))
selected_branch_nodes = {
str(prompt[switch_node_id]["inputs"][f"value{selected_value}"][0])
for switch_node_id, (_node_id, selected_value) in result_switches.items()
if (
switch_node_id in prompt
and isinstance(prompt[switch_node_id], dict)
and isinstance(prompt[switch_node_id].get("inputs"), dict)
and _is_api_ref(prompt[switch_node_id]["inputs"].get(f"value{selected_value}"))
)
}
roots.update(
_selected_output_descendant_roots(
prompt,
selected_branch_nodes,
_setup_output_surface_node_ids(setup),
)
)
if not roots:
return prompt
return _prune_prompt_from_roots(prompt, roots, selected_switches)
def _prune_prompt_from_roots(
prompt: dict[str, Any],
roots: set[str],
selected_switches: dict[str, tuple[str, int]],
) -> dict[str, Any]:
keep: set[str] = set()
def visit(node_id: str) -> None:
if node_id in keep or node_id not in prompt:
return
keep.add(node_id)
node = prompt.get(node_id)
if not isinstance(node, dict):
return
inputs = node.get("inputs")
if not isinstance(inputs, dict):
return
selected_value = selected_switches.get(node_id, (None, None))[1]
for input_name, value in inputs.items():
if (
selected_value is not None
and input_name.startswith("value")
and input_name != f"value{selected_value}"
):
continue
for ref in _api_refs(value):
visit(str(ref[0]))
for root in roots:
visit(root)
if not keep:
return prompt
pruned = {node_id: deepcopy(node) for node_id, node in prompt.items() if node_id in keep}
for node_id, (_switch_node_id, selected_value) in selected_switches.items():
node = pruned.get(node_id)
if not isinstance(node, dict):
continue
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
node["inputs"] = {
key: value
for key, value in inputs.items()
if not key.startswith("value") or key == f"value{selected_value}"
}
return pruned
def _selected_result_switches(
setup: dict[str, Any],
api_prompt: dict[str, Any],
run_inputs: dict[str, Any],
) -> dict[str, tuple[str, int]]:
app = setup.get("setupSurface", {}).get("app", {})
if not isinstance(app, dict):
return {}
switch = app.get("switch")
if not isinstance(switch, dict):
return {}
selected_value = _selected_switch_value(switch, run_inputs)
if selected_value is None:
return {}
selected: dict[str, tuple[str, int]] = {}
for field in _app_result_fields(setup):
result_node_id = _result_switch_node_id(setup, field, run_inputs, api_prompt)
if result_node_id is not None:
selected[result_node_id] = (result_node_id, selected_value)
return selected
def _selected_app_switches(
setup: dict[str, Any],
api_prompt: dict[str, Any],
run_inputs: dict[str, Any],
) -> dict[str, tuple[str, int]]:
app = setup.get("setupSurface", {}).get("app", {})
if not isinstance(app, dict):
return {}
switch = app.get("switch")
if not isinstance(switch, dict):
return {}
switch_target = switch.get("target")
if not isinstance(switch_target, dict):
return {}
selected_value = _selected_switch_value(switch, run_inputs)
if selected_value is None:
return {}
selected: dict[str, tuple[str, int]] = {}
for node_id, node in api_prompt.items():
if not isinstance(node, dict):
continue
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
selector_ref = _switch_selector_ref(inputs)
if not _selector_matches_switch_target(selector_ref, switch_target, api_prompt):
continue
if _is_api_ref(inputs.get(f"value{selected_value}")):
selected[str(node_id)] = (str(node_id), selected_value)
return selected
def _selected_output_descendant_roots(
api_prompt: dict[str, Any],
selected_branch_nodes: set[str],
output_surface_node_ids: set[str],
) -> set[str]:
if not selected_branch_nodes or not output_surface_node_ids:
return set()
children_by_node = _prompt_children_by_node(api_prompt)
roots: set[str] = set()
seen: set[str] = set()
pending = list(selected_branch_nodes)
while pending:
node_id = pending.pop()
if node_id in seen:
continue
seen.add(node_id)
for child_id in children_by_node.get(node_id, set()):
if child_id in output_surface_node_ids and child_id not in selected_branch_nodes:
roots.add(child_id)
pending.append(child_id)
return roots
def _prompt_children_by_node(api_prompt: dict[str, Any]) -> dict[str, set[str]]:
children: dict[str, set[str]] = {}
for node_id, node in api_prompt.items():
if not isinstance(node, dict):
continue
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
for value in inputs.values():
for ref in _api_refs(value):
children.setdefault(str(ref[0]), set()).add(str(node_id))
return children
def _setup_output_surface_node_ids(setup: dict[str, Any]) -> set[str]:
setup_surface = setup.get("setupSurface", {})
if not isinstance(setup_surface, dict):
return set()
output_ids: set[str] = set()
outputs = setup_surface.get("outputs")
if not isinstance(outputs, list):
return output_ids
for group in outputs:
if not isinstance(group, dict) or not isinstance(group.get("nodes"), list):
continue
for node in group["nodes"]:
if isinstance(node, dict) and node.get("id") is not None:
output_ids.add(str(node["id"]))
return output_ids
def _app_result_fields(setup: dict[str, Any]) -> list[dict[str, Any]]:
app = setup.get("setupSurface", {}).get("app", {})
if not isinstance(app, dict) or not isinstance(app.get("results"), list):
return []
return [field for field in app["results"] if isinstance(field, dict)]
def _result_switch_node_id(
setup: dict[str, Any],
result_field: dict[str, Any],
run_inputs: dict[str, Any],
api_prompt: dict[str, Any] | None = None,
) -> str | None:
app = setup.get("setupSurface", {}).get("app", {})
if not isinstance(app, dict):
return None
switch = app.get("switch")
if not isinstance(switch, dict):
return None
selected_value = _selected_switch_value(switch, run_inputs)
if selected_value is None:
return None
api_prompt = api_prompt if isinstance(api_prompt, dict) else setup.get("apiPrompt")
if not isinstance(api_prompt, dict):
return None
target = result_field.get("target")
if not isinstance(target, dict):
return None
result_node = api_prompt.get(str(target.get("node")))
if not isinstance(result_node, dict):
return None
result_inputs = result_node.get("inputs")
if not isinstance(result_inputs, dict):
return None
result_ref = result_inputs.get(str(target.get("input")))
if not _is_api_ref(result_ref):
return None
switch_node_id = str(result_ref[0])
switch_node = api_prompt.get(switch_node_id)
if not isinstance(switch_node, dict):
return None
switch_inputs = switch_node.get("inputs")
if not isinstance(switch_inputs, dict):
return None
selector_ref = _switch_selector_ref(switch_inputs)
switch_target = switch.get("target")
if isinstance(switch_target, dict) and not _selector_matches_switch_target(
selector_ref,
switch_target,
api_prompt,
):
return None
if not _is_api_ref(switch_inputs.get(f"value{selected_value}")):
return None
return switch_node_id
def _selected_result_branch_ref(
setup: dict[str, Any],
result_field: dict[str, Any],
run_inputs: dict[str, Any],
) -> list[Any] | None:
api_prompt = setup.get("apiPrompt")
if not isinstance(api_prompt, dict):
return None
switch_node_id = _result_switch_node_id(setup, result_field, run_inputs, api_prompt)
if switch_node_id is None:
return None
switch_node = api_prompt.get(switch_node_id)
if not isinstance(switch_node, dict):
return None
switch_inputs = switch_node.get("inputs")
if not isinstance(switch_inputs, dict):
return None
switch = setup.get("setupSurface", {}).get("app", {}).get("switch")
selected_value = _selected_switch_value(switch, run_inputs) if isinstance(switch, dict) else None
if selected_value is None:
return None
branch_ref = switch_inputs.get(f"value{selected_value}")
return branch_ref if _is_api_ref(branch_ref) else None
def _selected_switch_value(switch: dict[str, Any], run_inputs: dict[str, Any]) -> int | None:
key = switch.get("key")
selected = run_inputs.get(key) if isinstance(key, str) and key in run_inputs else switch.get("default")
return _switch_option_value(switch, selected)
def _selector_matches_switch_target(selector_ref: Any, switch_target: dict[str, Any], api_prompt: dict[str, Any]) -> bool:
if not _is_api_ref(selector_ref):
return False
source_node_id = str(selector_ref[0])
target_node_id = str(switch_target.get("node"))
if source_node_id != target_node_id:
return False
source_node = api_prompt.get(source_node_id)
if not isinstance(source_node, dict):
return False
outputs = _publish_input_output_slots(source_node)
return outputs.get("switch") == int(selector_ref[1])
def _switch_selector_ref(switch_inputs: dict[str, Any]) -> Any:
if "select" in switch_inputs:
return switch_inputs.get("select")
return switch_inputs.get("index")
def _publish_input_output_slots(api_node: dict[str, Any]) -> dict[str, int]:
if api_node.get("class_type") != "Koolook_PublishInput":
return {}
return {
"sequence_folder": 0,
"qt_file": 1,
"single_file": 2,
"prompt": 3,
"switch": 4,
}
def _resolve_prompt_value(prompt: dict[str, Any], value: Any) -> Any:
if not _is_api_ref(value):
return value
node_id = str(value[0])
output_index = int(value[1])
node = prompt.get(node_id)
if not isinstance(node, dict):
return None
class_type = node.get("class_type")
inputs = node.get("inputs")
if not isinstance(inputs, dict):
return None
if class_type == "Koolook_PublishOutput":
values = (
inputs.get("folder"),
inputs.get("name"),
inputs.get("version"),
)
return values[output_index] if output_index < len(values) else None
if class_type == "EasyAIPipeline":
if "WRITE_file_path" in inputs:
values = (
inputs.get("WRITE_file_path"),
inputs.get("output_name", ""),
inputs.get("version_string", ""),
inputs.get("output_directory", ""),
inputs.get("shot_duration", 0),
inputs.get("seed_value", 0),
inputs.get("shot_name", ""),
)
return values[output_index] if output_index < len(values) else None
resolved_inputs = {
key: _resolve_prompt_value(prompt, input_value)
for key, input_value in inputs.items()
}
try:
try:
from .k_ai_pipeline import build_pipeline_outputs
except ImportError: # pragma: no cover - standalone test/import context
from k_ai_pipeline import build_pipeline_outputs
values = build_pipeline_outputs(
**resolved_inputs,
create_directory=False,
check_overwrite=False,
)
except Exception:
return None
return values[output_index] if output_index < len(values) else None
return None
def _is_api_ref(value: Any) -> bool:
return (
isinstance(value, list)
and len(value) == 2
and isinstance(value[0], (str, int))
and not isinstance(value[1], bool)
and isinstance(value[1], int)
)
def _api_refs(value: Any) -> list[list[Any]]:
if _is_api_ref(value):
return [value]
if isinstance(value, dict):
refs: list[list[Any]] = []
for child in value.values():
refs.extend(_api_refs(child))
return refs
if isinstance(value, list):
refs = []
for child in value:
refs.extend(_api_refs(child))
return refs
return []
def _flatten_history_outputs(raw_outputs: Any) -> list[dict[str, Any]]:
if not isinstance(raw_outputs, dict):
return []
items: list[dict[str, Any]] = []
for node_id, node_outputs in raw_outputs.items():
if not isinstance(node_outputs, dict):
continue
for kind, values in node_outputs.items():
if not isinstance(values, list):
continue
for value in values:
item = {"nodeId": str(node_id), "kind": str(kind)}
if isinstance(value, dict):
item.update(value)
else:
item["value"] = value
items.append(item)
return items
def _history_output_matches_contract(item: dict[str, Any], contract_type: str) -> bool:
kind = str(item.get("kind", "")).lower()
if contract_type == "video":
return kind in {"video", "videos", "animated"}
if contract_type == "image":
return kind in {"image", "images"}
return True