feat: harden workflow graph contracts

This commit is contained in:
2026-04-08 21:32:14 +02:00
parent 22981af1d2
commit bd18cccb5e
7 changed files with 1403 additions and 100 deletions
@@ -19,7 +19,7 @@ _PRESET_TYPES = {
}
_EXECUTION_MODES = {"legacy", "graph", "shadow"}
_WORKFLOW_BLUEPRINTS = {"cad_intake", "order_rendering"}
_WORKFLOW_BLUEPRINTS = {"cad_intake", "order_rendering", "still_graph_reference"}
_WORKFLOW_STARTERS = {"cad_file", "order_line"}
_NODE_TYPE_TO_STEP: dict[str, str] = {
@@ -63,6 +63,50 @@ def _resolution_to_dimensions(params: dict[str, Any]) -> dict[str, Any]:
return merged
def _extract_render_params_from_nodes(nodes: list[dict[str, Any]], step: StepName) -> dict[str, Any]:
for node in nodes:
if node.get("step") == step.value:
return _resolution_to_dimensions(node.get("params") or {})
return {}
def _build_order_line_still_graph_nodes(render_params: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
graph_render_params = deepcopy(render_params)
graph_render_params.setdefault("use_custom_render_settings", True)
nodes = [
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 160, label="Order Line Setup"),
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 160, label="Resolve Template"),
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 220, 320, label="Auto Populate Materials"),
_make_node("bbox", StepName.GLB_BBOX, 220, 40, label="Compute Bounding Box"),
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 440, 200, label="Resolve Material Map"),
_make_node(
"render",
StepName.BLENDER_STILL,
680,
160,
params=graph_render_params,
node_type="renderNode",
label="Still Render",
),
_make_node("output", StepName.OUTPUT_SAVE, 920, 120, label="Save Output"),
_make_node("notify", StepName.NOTIFY, 920, 220, label="Notify Result"),
]
edges = [
{"from": "setup", "to": "template"},
{"from": "setup", "to": "populate_materials"},
{"from": "setup", "to": "bbox"},
{"from": "template", "to": "resolve_materials"},
{"from": "populate_materials", "to": "resolve_materials"},
{"from": "resolve_materials", "to": "render"},
{"from": "bbox", "to": "render"},
{"from": "template", "to": "render"},
{"from": "render", "to": "output"},
{"from": "render", "to": "notify"},
]
return nodes, edges
def build_preset_workflow_config(
preset_type: WorkflowPresetType,
params: dict[str, Any] | None = None,
@@ -94,31 +138,7 @@ def build_preset_workflow_config(
{"from": "render", "to": "output"},
]
elif preset_type == "still_graph":
nodes = [
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 220, 100, label="Auto Populate Materials"),
_make_node("template", StepName.RESOLVE_TEMPLATE, 440, 100, label="Resolve Template"),
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 660, 100, label="Resolve Material Map"),
_make_node(
"render",
StepName.BLENDER_STILL,
880,
100,
params=render_params,
node_type="renderNode",
label="Still Render",
),
_make_node("output", StepName.OUTPUT_SAVE, 1100, 70, label="Save Output"),
_make_node("notify", StepName.NOTIFY, 1100, 160, label="Notify Result"),
]
edges = [
{"from": "setup", "to": "populate_materials"},
{"from": "populate_materials", "to": "template"},
{"from": "template", "to": "resolve_materials"},
{"from": "resolve_materials", "to": "render"},
{"from": "render", "to": "output"},
{"from": "render", "to": "notify"},
]
nodes, edges = _build_order_line_still_graph_nodes(render_params)
elif preset_type == "turntable":
nodes = [
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
@@ -246,17 +266,17 @@ def build_workflow_blueprint_config(blueprint: str) -> dict[str, Any]:
{"from": "blender_thumb", "to": "save_blender_thumb"},
{"from": "threejs_thumb", "to": "save_threejs_thumb"},
]
else:
elif blueprint == "order_rendering":
nodes = [
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 220, label="Order Line Setup"),
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 220, label="Resolve Template"),
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 220, 360, label="Auto Populate Materials"),
_make_node("bbox", StepName.GLB_BBOX, 220, 80, label="Compute Bounding Box"),
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 440, 80, label="Resolve Material Map"),
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 660, 80, label="Auto Populate Materials"),
_make_node("template", StepName.RESOLVE_TEMPLATE, 880, 220, label="Resolve Template"),
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 440, 220, label="Resolve Material Map"),
_make_node(
"still_render",
StepName.BLENDER_STILL,
1120,
680,
80,
params={"rotation_z": 0},
node_type="renderNode",
@@ -265,33 +285,42 @@ def build_workflow_blueprint_config(blueprint: str) -> dict[str, Any]:
_make_node(
"turntable_render",
StepName.BLENDER_TURNTABLE,
1120,
680,
220,
params={"fps": 24, "duration_s": 5},
node_type="renderFramesNode",
label="Render Turntable",
),
_make_node("blend_export", StepName.EXPORT_BLEND, 1120, 360, label="Export Blend"),
_make_node("save_still", StepName.OUTPUT_SAVE, 1360, 80, label="Save Still Output"),
_make_node("save_turntable", StepName.OUTPUT_SAVE, 1360, 220, label="Save Turntable Output"),
_make_node("notify_still", StepName.NOTIFY, 1600, 80, label="Notify Still Result"),
_make_node("notify_turntable", StepName.NOTIFY, 1600, 220, label="Notify Turntable Result"),
_make_node("notify_export", StepName.NOTIFY, 1360, 360, label="Notify Blend Export"),
_make_node("blend_export", StepName.EXPORT_BLEND, 680, 360, label="Export Blend"),
_make_node("save_still", StepName.OUTPUT_SAVE, 920, 80, label="Save Still Output"),
_make_node("save_turntable", StepName.OUTPUT_SAVE, 920, 220, label="Save Turntable Output"),
_make_node("notify_still", StepName.NOTIFY, 920, 140, label="Notify Still Result"),
_make_node("notify_turntable", StepName.NOTIFY, 920, 280, label="Notify Turntable Result"),
_make_node("notify_export", StepName.NOTIFY, 920, 360, label="Notify Blend Export"),
]
edges = [
{"from": "setup", "to": "template"},
{"from": "setup", "to": "populate_materials"},
{"from": "setup", "to": "bbox"},
{"from": "bbox", "to": "resolve_materials"},
{"from": "resolve_materials", "to": "populate_materials"},
{"from": "populate_materials", "to": "template"},
{"from": "template", "to": "resolve_materials"},
{"from": "populate_materials", "to": "resolve_materials"},
{"from": "resolve_materials", "to": "still_render"},
{"from": "resolve_materials", "to": "turntable_render"},
{"from": "bbox", "to": "still_render"},
{"from": "bbox", "to": "turntable_render"},
{"from": "template", "to": "still_render"},
{"from": "template", "to": "turntable_render"},
{"from": "template", "to": "blend_export"},
{"from": "still_render", "to": "save_still"},
{"from": "still_render", "to": "notify_still"},
{"from": "turntable_render", "to": "save_turntable"},
{"from": "save_still", "to": "notify_still"},
{"from": "save_turntable", "to": "notify_turntable"},
{"from": "turntable_render", "to": "notify_turntable"},
{"from": "blend_export", "to": "notify_export"},
]
elif blueprint == "still_graph_reference":
nodes, edges = _build_order_line_still_graph_nodes(
{"render_engine": "cycles", "samples": 256, "width": 1920, "height": 1080}
)
return {
"version": 1,
@@ -299,7 +328,7 @@ def build_workflow_blueprint_config(blueprint: str) -> dict[str, Any]:
"edges": edges,
"ui": {
"preset": "custom",
"execution_mode": "legacy",
"execution_mode": "graph" if blueprint == "still_graph_reference" else "legacy",
"blueprint": blueprint,
},
}
@@ -438,6 +467,26 @@ def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
ui = {}
normalized["ui"] = dict(ui)
normalized["ui"].setdefault("execution_mode", "legacy")
preset = normalized["ui"].get("preset")
blueprint = normalized["ui"].get("blueprint")
if preset == "still_graph":
merged_ui = dict(normalized["ui"])
canonical = build_preset_workflow_config(
"still_graph",
_extract_render_params_from_nodes(normalized.get("nodes") or [], StepName.BLENDER_STILL),
)
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui)
return canonical
if blueprint == "still_graph_reference":
merged_ui = dict(normalized["ui"])
canonical = build_workflow_blueprint_config("still_graph_reference")
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui)
return canonical
return normalized
workflow_type = raw.get("type")
@@ -451,6 +500,9 @@ def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
merged_ui.update(raw_ui)
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
merged_ui["execution_mode"] = "legacy"
if workflow_type == "still" and merged_ui.get("execution_mode") == "graph":
canonical = build_preset_workflow_config("still_graph", raw.get("params") or {})
merged_ui = dict(canonical.get("ui") or {}) | merged_ui
canonical["ui"] = merged_ui
return canonical