592 lines
22 KiB
Python
592 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any
|
|
|
|
from app.core.process_steps import StepName
|
|
from app.domains.rendering.workflow_node_registry import get_node_type_for_step
|
|
|
|
|
|
WorkflowPresetType = str
|
|
|
|
_PRESET_TYPES = {
|
|
"still",
|
|
"still_graph",
|
|
"turntable",
|
|
"multi_angle",
|
|
"still_with_exports",
|
|
"custom",
|
|
}
|
|
|
|
_EXECUTION_MODES = {"legacy", "graph", "shadow"}
|
|
_WORKFLOW_BLUEPRINTS = {"cad_intake", "order_rendering", "still_graph_reference"}
|
|
_WORKFLOW_STARTERS = {"cad_file", "order_line"}
|
|
_WORKFLOW_STARTER_BLUEPRINTS = {
|
|
"starter_cad_intake": "cad_file",
|
|
"starter_order_rendering": "order_line",
|
|
}
|
|
|
|
_NODE_TYPE_TO_STEP: dict[str, str] = {
|
|
"inputNode": StepName.RESOLVE_STEP_PATH.value,
|
|
"convertNode": StepName.STL_CACHE_GENERATE.value,
|
|
"processNode": StepName.ORDER_LINE_SETUP.value,
|
|
"renderNode": StepName.BLENDER_STILL.value,
|
|
"renderFramesNode": StepName.BLENDER_TURNTABLE.value,
|
|
"ffmpegNode": StepName.OUTPUT_SAVE.value,
|
|
"outputNode": StepName.OUTPUT_SAVE.value,
|
|
}
|
|
|
|
def _make_node(
|
|
node_id: str,
|
|
step: StepName,
|
|
x: int,
|
|
y: int,
|
|
*,
|
|
params: dict[str, Any] | None = None,
|
|
node_type: str | None = None,
|
|
label: str | None = None,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": node_id,
|
|
"step": step.value,
|
|
"params": deepcopy(params or {}),
|
|
"ui": {
|
|
"type": node_type or get_node_type_for_step(step.value),
|
|
"position": {"x": x, "y": y},
|
|
"label": label,
|
|
},
|
|
}
|
|
|
|
|
|
def _resolution_to_dimensions(params: dict[str, Any]) -> dict[str, Any]:
|
|
merged = deepcopy(params)
|
|
resolution = merged.pop("resolution", None)
|
|
if isinstance(resolution, (list, tuple)) and len(resolution) == 2:
|
|
merged.setdefault("width", int(resolution[0]))
|
|
merged.setdefault("height", int(resolution[1]))
|
|
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", False)
|
|
|
|
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,
|
|
) -> dict[str, Any]:
|
|
if preset_type not in _PRESET_TYPES:
|
|
raise ValueError(f"Unknown workflow preset type: {preset_type!r}")
|
|
|
|
params = deepcopy(params or {})
|
|
render_params = _resolution_to_dimensions(params)
|
|
|
|
if preset_type == "still":
|
|
nodes = [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
|
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 100, label="Resolve Template"),
|
|
_make_node(
|
|
"render",
|
|
StepName.BLENDER_STILL,
|
|
440,
|
|
100,
|
|
params=render_params,
|
|
node_type="renderNode",
|
|
label="Still Render",
|
|
),
|
|
_make_node("output", StepName.OUTPUT_SAVE, 660, 100, label="Save Output"),
|
|
]
|
|
edges = [
|
|
{"from": "setup", "to": "template"},
|
|
{"from": "template", "to": "render"},
|
|
{"from": "render", "to": "output"},
|
|
]
|
|
elif preset_type == "still_graph":
|
|
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"),
|
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 100, label="Resolve Template"),
|
|
_make_node(
|
|
"turntable",
|
|
StepName.BLENDER_TURNTABLE,
|
|
440,
|
|
100,
|
|
params=render_params,
|
|
node_type="renderFramesNode",
|
|
label="Turntable Render",
|
|
),
|
|
_make_node("output", StepName.OUTPUT_SAVE, 660, 100, label="Save Output"),
|
|
]
|
|
edges = [
|
|
{"from": "setup", "to": "template"},
|
|
{"from": "template", "to": "turntable"},
|
|
{"from": "turntable", "to": "output"},
|
|
]
|
|
elif preset_type == "multi_angle":
|
|
angles = params.get("angles") or [0, 45, 90]
|
|
shared = deepcopy(render_params)
|
|
shared.pop("angles", None)
|
|
nodes = [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 195, label="Order Line Setup"),
|
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 195, label="Resolve Template"),
|
|
]
|
|
edges = [
|
|
{"from": "setup", "to": "template"},
|
|
]
|
|
for index, angle in enumerate(angles):
|
|
node_id = f"render_{index}"
|
|
node_params = {**shared, "rotation_z": float(angle)}
|
|
nodes.append(
|
|
_make_node(
|
|
node_id,
|
|
StepName.BLENDER_STILL,
|
|
440,
|
|
index * 130,
|
|
params=node_params,
|
|
node_type="renderNode",
|
|
label=f"Render {angle}°",
|
|
)
|
|
)
|
|
edges.append({"from": "template", "to": node_id})
|
|
nodes.append(_make_node("output", StepName.OUTPUT_SAVE, 700, 195, label="Save Output"))
|
|
edges.extend({"from": f"render_{index}", "to": "output"} for index, _ in enumerate(angles))
|
|
elif preset_type == "still_with_exports":
|
|
nodes = [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
|
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 220, 100, label="Resolve Template"),
|
|
_make_node(
|
|
"render",
|
|
StepName.BLENDER_STILL,
|
|
440,
|
|
100,
|
|
params=render_params,
|
|
node_type="renderNode",
|
|
label="Still Render",
|
|
),
|
|
_make_node("output", StepName.OUTPUT_SAVE, 660, 70, label="Save Output"),
|
|
_make_node("blend", StepName.EXPORT_BLEND, 660, 160, label="Export Blend"),
|
|
]
|
|
edges = [
|
|
{"from": "setup", "to": "template"},
|
|
{"from": "template", "to": "render"},
|
|
{"from": "render", "to": "output"},
|
|
{"from": "render", "to": "blend"},
|
|
]
|
|
else:
|
|
nodes = [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 120, 140, label="Order Line Setup"),
|
|
]
|
|
edges = []
|
|
|
|
return {
|
|
"version": 1,
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"ui": {
|
|
"preset": preset_type,
|
|
"execution_mode": "graph" if preset_type == "still_graph" else "legacy",
|
|
"family": "order_line",
|
|
},
|
|
}
|
|
|
|
|
|
def build_workflow_blueprint_config(blueprint: str) -> dict[str, Any]:
|
|
if blueprint not in _WORKFLOW_BLUEPRINTS:
|
|
raise ValueError(f"Unknown workflow blueprint: {blueprint!r}")
|
|
|
|
if blueprint == "cad_intake":
|
|
nodes = [
|
|
_make_node("resolve_step", StepName.RESOLVE_STEP_PATH, 0, 180, label="Resolve STEP Path"),
|
|
_make_node("extract_objects", StepName.OCC_OBJECT_EXTRACT, 220, 180, label="Extract STEP Objects"),
|
|
_make_node("export_glb", StepName.OCC_GLB_EXPORT, 440, 180, label="Export GLB"),
|
|
_make_node("bbox", StepName.GLB_BBOX, 660, 120, label="Compute Bounding Box"),
|
|
_make_node("stl_cache", StepName.STL_CACHE_GENERATE, 660, 300, label="Generate STL Cache"),
|
|
_make_node(
|
|
"blender_thumb",
|
|
StepName.BLENDER_RENDER,
|
|
880,
|
|
120,
|
|
params={"render_engine": "cycles", "samples": 64, "width": 512, "height": 512},
|
|
node_type="renderNode",
|
|
label="Render Thumbnail (Blender)",
|
|
),
|
|
_make_node(
|
|
"threejs_thumb",
|
|
StepName.THREEJS_RENDER,
|
|
880,
|
|
320,
|
|
params={"width": 512, "height": 512, "transparent_bg": True},
|
|
node_type="renderNode",
|
|
label="Render Thumbnail (Three.js)",
|
|
),
|
|
_make_node("save_blender_thumb", StepName.THUMBNAIL_SAVE, 1100, 120, label="Save Blender Thumbnail"),
|
|
_make_node("save_threejs_thumb", StepName.THUMBNAIL_SAVE, 1100, 320, label="Save Three.js Thumbnail"),
|
|
]
|
|
edges = [
|
|
{"from": "resolve_step", "to": "extract_objects"},
|
|
{"from": "extract_objects", "to": "export_glb"},
|
|
{"from": "export_glb", "to": "bbox"},
|
|
{"from": "export_glb", "to": "stl_cache"},
|
|
{"from": "export_glb", "to": "blender_thumb"},
|
|
{"from": "export_glb", "to": "threejs_thumb"},
|
|
{"from": "bbox", "to": "threejs_thumb"},
|
|
{"from": "blender_thumb", "to": "save_blender_thumb"},
|
|
{"from": "threejs_thumb", "to": "save_threejs_thumb"},
|
|
]
|
|
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, 220, label="Resolve Material Map"),
|
|
_make_node(
|
|
"still_render",
|
|
StepName.BLENDER_STILL,
|
|
680,
|
|
80,
|
|
params={"rotation_z": 0},
|
|
node_type="renderNode",
|
|
label="Render Still",
|
|
),
|
|
_make_node(
|
|
"turntable_render",
|
|
StepName.BLENDER_TURNTABLE,
|
|
680,
|
|
220,
|
|
params={"fps": 24, "duration_s": 5},
|
|
node_type="renderFramesNode",
|
|
label="Render Turntable",
|
|
),
|
|
_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": "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": "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,
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"ui": {
|
|
"preset": "custom",
|
|
"execution_mode": "graph" if blueprint == "still_graph_reference" else "legacy",
|
|
"family": "cad_file" if blueprint == "cad_intake" else "order_line",
|
|
"blueprint": blueprint,
|
|
},
|
|
}
|
|
|
|
|
|
def build_starter_workflow_config(family: str = "order_line") -> dict[str, Any]:
|
|
if family not in _WORKFLOW_STARTERS:
|
|
raise ValueError(f"Unknown workflow starter family: {family!r}")
|
|
|
|
if family == "cad_file":
|
|
nodes = [
|
|
_make_node("resolve_step", StepName.RESOLVE_STEP_PATH, 120, 140, label="Resolve STEP Path"),
|
|
]
|
|
blueprint = "starter_cad_intake"
|
|
else:
|
|
nodes = [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 120, 140, label="Order Line Setup"),
|
|
]
|
|
blueprint = "starter_order_rendering"
|
|
|
|
return {
|
|
"version": 1,
|
|
"nodes": nodes,
|
|
"edges": [],
|
|
"ui": {
|
|
"preset": "custom",
|
|
"execution_mode": "legacy",
|
|
"family": family,
|
|
"blueprint": blueprint,
|
|
},
|
|
}
|
|
|
|
|
|
def _build_legacy_custom_render_fallback_config(params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
render_params = _resolution_to_dimensions(params or {})
|
|
render_params.setdefault("use_custom_render_settings", True)
|
|
|
|
return {
|
|
"version": 1,
|
|
"nodes": [
|
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 140, label="Order Line Setup"),
|
|
_make_node(
|
|
"render",
|
|
StepName.BLENDER_STILL,
|
|
240,
|
|
140,
|
|
params=render_params,
|
|
node_type="renderNode",
|
|
label="Still Render",
|
|
),
|
|
],
|
|
"edges": [
|
|
{"from": "setup", "to": "render"},
|
|
],
|
|
"ui": {
|
|
"preset": "custom",
|
|
"execution_mode": "legacy",
|
|
"family": "order_line",
|
|
"blueprint": "starter_order_rendering",
|
|
},
|
|
}
|
|
|
|
|
|
def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
|
legacy_nodes = raw.get("nodes") or []
|
|
legacy_edges = raw.get("edges") or []
|
|
raw_ui = raw.get("ui")
|
|
|
|
if not legacy_nodes:
|
|
canonical = _build_legacy_custom_render_fallback_config(raw.get("params") or {})
|
|
if isinstance(raw_ui, dict):
|
|
merged_ui = dict(canonical.get("ui") or {})
|
|
merged_ui.update(raw_ui)
|
|
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
|
|
merged_ui["execution_mode"] = "legacy"
|
|
canonical["ui"] = merged_ui
|
|
return canonical
|
|
|
|
nodes: list[dict[str, Any]] = []
|
|
for legacy_node in legacy_nodes:
|
|
data = legacy_node.get("data") or {}
|
|
node_type = legacy_node.get("type")
|
|
step_name = data.get("pipeline_step") or _NODE_TYPE_TO_STEP.get(node_type) or StepName.BLENDER_STILL.value
|
|
nodes.append(
|
|
{
|
|
"id": legacy_node["id"],
|
|
"step": step_name,
|
|
"params": deepcopy(data.get("params") or {}),
|
|
"ui": {
|
|
"type": node_type,
|
|
"position": deepcopy(legacy_node.get("position") or {"x": 0, "y": 0}),
|
|
"label": data.get("label"),
|
|
},
|
|
}
|
|
)
|
|
|
|
edges: list[dict[str, Any]] = []
|
|
for index, legacy_edge in enumerate(legacy_edges):
|
|
source = legacy_edge.get("source") or legacy_edge.get("from")
|
|
target = legacy_edge.get("target") or legacy_edge.get("to")
|
|
if not source or not target:
|
|
continue
|
|
edges.append(
|
|
{
|
|
"id": legacy_edge.get("id") or f"edge_{index}",
|
|
"from": source,
|
|
"to": target,
|
|
}
|
|
)
|
|
|
|
canonical = {
|
|
"version": 1,
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"ui": {
|
|
"preset": "custom",
|
|
"execution_mode": "legacy",
|
|
},
|
|
}
|
|
if isinstance(raw_ui, dict):
|
|
merged_ui = dict(canonical.get("ui") or {})
|
|
merged_ui.update(raw_ui)
|
|
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
|
|
merged_ui["execution_mode"] = "legacy"
|
|
canonical["ui"] = merged_ui
|
|
return canonical
|
|
|
|
|
|
def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("Workflow config must be a JSON object")
|
|
|
|
if "version" in raw and "nodes" in raw:
|
|
normalized = deepcopy(raw)
|
|
normalized.setdefault("edges", [])
|
|
ui = normalized.get("ui")
|
|
if not isinstance(ui, dict):
|
|
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 in _WORKFLOW_BLUEPRINTS:
|
|
merged_ui = dict(normalized["ui"])
|
|
canonical = build_workflow_blueprint_config(blueprint)
|
|
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
|
canonical["ui"].update(merged_ui)
|
|
return canonical
|
|
|
|
if blueprint in _WORKFLOW_STARTER_BLUEPRINTS:
|
|
merged_ui = dict(normalized["ui"])
|
|
canonical = build_starter_workflow_config(_WORKFLOW_STARTER_BLUEPRINTS[blueprint])
|
|
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
|
canonical["ui"].update(merged_ui)
|
|
return canonical
|
|
|
|
return normalized
|
|
|
|
workflow_type = raw.get("type")
|
|
if workflow_type in _PRESET_TYPES:
|
|
if workflow_type == "custom":
|
|
return _canonicalize_legacy_custom_config(raw)
|
|
canonical = build_preset_workflow_config(workflow_type, raw.get("params") or {})
|
|
raw_ui = raw.get("ui")
|
|
if isinstance(raw_ui, dict):
|
|
merged_ui = dict(canonical.get("ui") or {})
|
|
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
|
|
|
|
raise ValueError("Unsupported workflow config format")
|
|
|
|
|
|
def workflow_config_requires_canonicalization(raw: dict[str, Any]) -> bool:
|
|
if not isinstance(raw, dict):
|
|
return True
|
|
|
|
if "version" not in raw or "nodes" not in raw:
|
|
return True
|
|
|
|
return raw != canonicalize_workflow_config(raw)
|
|
|
|
|
|
def get_workflow_preset_type(config: dict[str, Any]) -> str | None:
|
|
canonical = canonicalize_workflow_config(config)
|
|
ui = canonical.get("ui") or {}
|
|
preset = ui.get("preset")
|
|
if preset in _PRESET_TYPES:
|
|
return preset
|
|
return None
|
|
|
|
|
|
def get_workflow_execution_mode(config: dict[str, Any], *, default: str = "legacy") -> str:
|
|
canonical = canonicalize_workflow_config(config)
|
|
ui = canonical.get("ui") or {}
|
|
mode = ui.get("execution_mode")
|
|
if mode in _EXECUTION_MODES:
|
|
return mode
|
|
return default
|
|
|
|
|
|
def extract_runtime_workflow(config: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
|
|
canonical = canonicalize_workflow_config(config)
|
|
preset = get_workflow_preset_type(canonical)
|
|
if preset is None or preset == "custom":
|
|
return preset, {}
|
|
|
|
nodes = canonical.get("nodes") or []
|
|
|
|
if preset in {"still", "still_graph", "still_with_exports"}:
|
|
for node in nodes:
|
|
if node.get("step") == StepName.BLENDER_STILL.value:
|
|
return preset, _resolution_to_dimensions(node.get("params") or {})
|
|
return preset, {}
|
|
|
|
if preset == "turntable":
|
|
for node in nodes:
|
|
if node.get("step") == StepName.BLENDER_TURNTABLE.value:
|
|
return preset, _resolution_to_dimensions(node.get("params") or {})
|
|
return preset, {}
|
|
|
|
if preset == "multi_angle":
|
|
render_nodes = [node for node in nodes if node.get("step") == StepName.BLENDER_STILL.value]
|
|
if not render_nodes:
|
|
return preset, {}
|
|
first_params = _resolution_to_dimensions(render_nodes[0].get("params") or {})
|
|
angles = [
|
|
float((node.get("params") or {}).get("rotation_z", 0))
|
|
for node in render_nodes
|
|
]
|
|
first_params["angles"] = angles
|
|
first_params.pop("rotation_z", None)
|
|
return preset, first_params
|
|
|
|
return preset, {}
|