feat(setups): split publish result contract

This commit is contained in:
malkuthro
2026-06-07 21:54:23 +02:00
parent af7052be61
commit 38cbc5bf61
5 changed files with 103 additions and 18 deletions
+11 -4
View File
@@ -244,6 +244,7 @@ nodes when a setup should be callable from an external app:
```text
Koolook Publish Input -> place in Koolook Input
Koolook Publish Output -> place in Koolook Output
Koolook Publish Result -> place in Koolook Output
```
`Koolook Publish Input` exposes stable multiline fields and outputs:
@@ -263,14 +264,20 @@ switch INT output derived from mode
folder STRING
name STRING
version STRING
```
`Koolook Publish Result` exposes the resolved result value after workflow
writer/path logic has run:
```text
result STRING
```
Publish detects these node classes and stores `setupSurface.app` with stable
keys, user-facing labels, defaults, injection targets, and switch options. The
external app should render the switch first, preserve the numeric switch values,
and hide internal-only options such as Prompt while keeping their index stable
for the workflow.
keys, user-facing labels, defaults, injection targets, result targets, and
switch options. The external app should render the switch first, preserve the
numeric switch values, and hide internal-only options such as Prompt while
keeping their index stable for the workflow.
## Callable Visual Workflow Standard
+27 -6
View File
@@ -83,7 +83,7 @@ class Koolook_PublishInput:
class Koolook_PublishOutput:
"""App-facing setup output controls and result path placeholder."""
"""App-facing setup output controls."""
@classmethod
def INPUT_TYPES(cls):
@@ -113,32 +113,53 @@ class Koolook_PublishOutput:
"tooltip": "Output version token or number.",
},
),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("folder", "name", "version")
CATEGORY = "Koolook/Publish"
FUNCTION = "run"
def run(self, folder: str, name: str, version: str):
return (folder, name, version)
class Koolook_PublishResult:
"""Resolved setup result surfaced back to the external app."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"result": (
"STRING",
{
"default": "",
"multiline": True,
"tooltip": "Result path/status surfaced back to the external app.",
"tooltip": "Final resolved output path, folder, or status from the workflow.",
},
),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("folder", "name", "version", "result")
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result",)
CATEGORY = "Koolook/Publish"
FUNCTION = "run"
def run(self, folder: str, name: str, version: str, result: str):
return (folder, name, version, result)
def run(self, result: str):
return (result,)
NODE_CLASS_MAPPINGS = {
"Koolook_PublishInput": Koolook_PublishInput,
"Koolook_PublishOutput": Koolook_PublishOutput,
"Koolook_PublishResult": Koolook_PublishResult,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Koolook_PublishInput": "Koolook Publish Input",
"Koolook_PublishOutput": "Koolook Publish Output",
"Koolook_PublishResult": "Koolook Publish Result",
}
+25 -1
View File
@@ -27,6 +27,7 @@ SETUP_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
VALIDATION_STATUSES = {"valid", "draft", "invalid"}
PUBLISH_INPUT_CLASS = "Koolook_PublishInput"
PUBLISH_OUTPUT_CLASS = "Koolook_PublishOutput"
PUBLISH_RESULT_CLASS = "Koolook_PublishResult"
PUBLISH_INPUT_FIELDS = (
("sequence_folder", "Sequence folder", True),
("qt_file", "QT file", True),
@@ -43,12 +44,15 @@ PUBLISH_OUTPUT_FIELDS = (
("folder", "Output folder", True),
("name", "Output name", True),
("version", "Version", True),
)
PUBLISH_RESULT_FIELDS = (
("result", "Result", True),
)
WIDGET_ONLY_INPUTS_BY_CLASS = {
"Text Multiline": ("text",),
"Koolook_PublishInput": ("mode", "sequence_folder", "qt_file", "single_file", "prompt"),
"Koolook_PublishOutput": ("folder", "name", "version", "result"),
"Koolook_PublishOutput": ("folder", "name", "version"),
"Koolook_PublishResult": ("result",),
"EasyAIPipeline": (
"shot_duration",
"seed_value",
@@ -376,11 +380,13 @@ def _infer_app_surface(visual_graph: dict[str, Any]) -> dict[str, Any]:
output_nodes = _nodes_in_group(visual_graph, "Koolook Output")
input_node = _first_node_of_type(input_nodes, PUBLISH_INPUT_CLASS)
output_node = _first_node_of_type(output_nodes, PUBLISH_OUTPUT_CLASS)
result_node = _first_node_of_type(output_nodes, PUBLISH_RESULT_CLASS)
inputs = _publish_input_fields(input_node)
outputs = _publish_output_fields(output_node)
app: dict[str, Any] = {
"inputs": inputs,
"outputs": outputs,
"results": _publish_result_fields(result_node),
}
if input_node is not None:
app["switch"] = _publish_input_switch(input_node, inputs)
@@ -433,6 +439,24 @@ def _publish_output_fields(node: dict[str, Any] | None) -> list[dict[str, Any]]:
]
def _publish_result_fields(node: dict[str, Any] | None) -> list[dict[str, Any]]:
if node is None:
return []
return [
{
"key": key,
"label": label,
"visible": visible,
"target": {
"node": str(node.get("id")),
"input": key,
},
"default": _app_widget_value(node, key),
}
for key, label, visible in PUBLISH_RESULT_FIELDS
]
def _publish_input_switch(
node: dict[str, Any],
inputs: list[dict[str, Any]],
+16 -4
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from k_publish_contract import (
Koolook_PublishInput,
Koolook_PublishOutput,
Koolook_PublishResult,
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS,
)
@@ -13,8 +14,10 @@ from k_publish_contract import (
def test_publish_contract_nodes_register() -> None:
assert NODE_CLASS_MAPPINGS["Koolook_PublishInput"] is Koolook_PublishInput
assert NODE_CLASS_MAPPINGS["Koolook_PublishOutput"] is Koolook_PublishOutput
assert NODE_CLASS_MAPPINGS["Koolook_PublishResult"] is Koolook_PublishResult
assert NODE_DISPLAY_NAME_MAPPINGS["Koolook_PublishInput"] == "Koolook Publish Input"
assert NODE_DISPLAY_NAME_MAPPINGS["Koolook_PublishOutput"] == "Koolook Publish Output"
assert NODE_DISPLAY_NAME_MAPPINGS["Koolook_PublishResult"] == "Koolook Publish Result"
def test_publish_input_exposes_stable_fields_and_switch_output() -> None:
@@ -44,11 +47,20 @@ def test_publish_input_exposes_stable_fields_and_switch_output() -> None:
def test_publish_output_exposes_stable_fields() -> None:
spec = Koolook_PublishOutput.INPUT_TYPES()["required"]
assert list(spec) == ["folder", "name", "version", "result"]
assert Koolook_PublishOutput.RETURN_NAMES == ("folder", "name", "version", "result")
assert list(spec) == ["folder", "name", "version"]
assert Koolook_PublishOutput.RETURN_NAMES == ("folder", "name", "version")
assert Koolook_PublishOutput().run(
folder="/out",
name="mask",
version="1",
result="/out/mask_v001.png",
) == ("/out", "mask", "1", "/out/mask_v001.png")
) == ("/out", "mask", "1")
def test_publish_result_exposes_resolved_result() -> None:
spec = Koolook_PublishResult.INPUT_TYPES()["required"]
assert list(spec) == ["result"]
assert Koolook_PublishResult.RETURN_NAMES == ("result",)
assert Koolook_PublishResult().run(result="/out/mask_v001.png") == (
"/out/mask_v001.png",
)
+24 -3
View File
@@ -778,7 +778,7 @@ def test_publish_setup_infers_app_surface_from_koolook_groups() -> None:
}
],
"controls": [],
"app": {"inputs": [], "outputs": []},
"app": {"inputs": [], "outputs": [], "results": []},
}
@@ -820,12 +820,31 @@ def test_publish_setup_infers_app_contract_from_publish_nodes() -> None:
"/shots/example/output",
"publish-OUT",
"1",
"/shots/example/output/publish-OUT_v001.mov",
],
"outputs": [
{"name": "folder", "type": "STRING", "links": []},
{"name": "name", "type": "STRING", "links": []},
{"name": "version", "type": "STRING", "links": []},
],
},
{
"id": 300,
"type": "Koolook_PublishResult",
"title": "Koolook Publish Result",
"pos": [520, 340],
"size": [360, 160],
"inputs": [
{
"name": "result",
"type": "STRING",
"widget": {"name": "result"},
"link": None,
}
],
"widgets_values": [
"/shots/example/output/publish-OUT_v001.mov",
],
"outputs": [
{"name": "result", "type": "STRING", "links": []},
],
},
@@ -905,11 +924,13 @@ def test_publish_setup_infers_app_contract_from_publish_nodes() -> None:
"target": {"node": "200", "input": "version"},
"default": "1",
},
],
"results": [
{
"key": "result",
"label": "Result",
"visible": True,
"target": {"node": "200", "input": "result"},
"target": {"node": "300", "input": "result"},
"default": "/shots/example/output/publish-OUT_v001.mov",
},
],