diff --git a/docs/maintainers/published-setups.md b/docs/maintainers/published-setups.md index 56d78e6..d8863cf 100644 --- a/docs/maintainers/published-setups.md +++ b/docs/maintainers/published-setups.md @@ -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 diff --git a/k_publish_contract.py b/k_publish_contract.py index b7f7a39..5a3fcca 100644 --- a/k_publish_contract.py +++ b/k_publish_contract.py @@ -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", } diff --git a/koolook_setups.py b/koolook_setups.py index fc59373..36dbd2f 100644 --- a/koolook_setups.py +++ b/koolook_setups.py @@ -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]], diff --git a/tests/nodes/test_publish_contract.py b/tests/nodes/test_publish_contract.py index 8f369c0..70b5165 100644 --- a/tests/nodes/test_publish_contract.py +++ b/tests/nodes/test_publish_contract.py @@ -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", + ) diff --git a/tests/server/test_published_setup_registry.py b/tests/server/test_published_setup_registry.py index fb77dad..d363854 100644 --- a/tests/server/test_published_setup_registry.py +++ b/tests/server/test_published_setup_registry.py @@ -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", }, ],