Files
HartOMat/backend/app/domains/rendering/workflow_config_utils.py
T

291 lines
9.8 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",
"turntable",
"multi_angle",
"still_with_exports",
"custom",
}
_EXECUTION_MODES = {"legacy", "graph", "shadow"}
_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 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 == "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},
}
def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
legacy_nodes = raw.get("nodes") or []
legacy_edges = raw.get("edges") or []
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,
}
)
return {
"version": 1,
"nodes": nodes,
"edges": edges,
"ui": {"preset": "custom"},
}
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", [])
return normalized
workflow_type = raw.get("type")
if workflow_type in _PRESET_TYPES:
if workflow_type == "custom":
return _canonicalize_legacy_custom_config(raw)
return build_preset_workflow_config(workflow_type, raw.get("params") or {})
raise ValueError("Unsupported workflow config format")
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_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, {}