feat: extract workflow output save phase 3

This commit is contained in:
2026-04-07 09:50:58 +02:00
parent 9c93ecef49
commit 160c198bb3
5 changed files with 232 additions and 81 deletions
@@ -99,6 +99,15 @@ class BBoxResolutionResult:
return self.bbox_data is not None
@dataclass(slots=True)
class OutputSaveResult:
status: Literal["completed", "failed"]
result_path: str | None
asset_id: str | None = None
storage_key: str | None = None
asset_type: MediaAssetType | None = None
def _emit(emit: EmitFn, order_line_id: str, message: str, level: str | None = None) -> None:
if emit is None:
return
@@ -197,6 +206,100 @@ def resolve_cad_bbox(
)
def _normalize_storage_key(output_path: str) -> str:
upload_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
return output_path[len(upload_prefix):] if output_path.startswith(upload_prefix) else output_path
def _resolve_output_asset_type(output_path: str) -> MediaAssetType:
extension = output_path.rsplit(".", 1)[-1].lower() if "." in output_path else "bin"
return MediaAssetType.turntable if extension in ("mp4", "webm") else MediaAssetType.still
def _resolve_output_mime_type(output_path: str) -> str:
extension = output_path.rsplit(".", 1)[-1].lower() if "." in output_path else "bin"
if extension in ("mp4", "webm"):
return "video/mp4"
if extension == "webp":
return "image/webp"
if extension in ("jpg", "jpeg"):
return "image/jpeg"
return "image/png"
def persist_order_line_output(
session: Session,
line: OrderLine,
*,
success: bool,
output_path: str,
render_log: dict[str, Any] | None,
render_completed_at: datetime | None = None,
) -> OutputSaveResult:
"""Persist the render result for an order line and publish the media asset if needed."""
status: Literal["completed", "failed"] = "completed" if success else "failed"
completed_at = render_completed_at or datetime.utcnow()
line.render_status = status
line.render_completed_at = completed_at
line.render_log = render_log
line.result_path = output_path if success else None
session.flush()
asset_id: str | None = None
storage_key: str | None = None
asset_type: MediaAssetType | None = None
if success:
storage_key = _normalize_storage_key(output_path)
asset_type = _resolve_output_asset_type(output_path)
existing_asset = session.execute(
select(MediaAsset).where(MediaAsset.storage_key == storage_key).limit(1)
).scalar_one_or_none()
if existing_asset is None:
output_file = Path(output_path)
render_config = None
if isinstance(render_log, dict):
render_config = {
key: render_log[key]
for key in (
"renderer",
"engine_used",
"engine",
"samples",
"device_used",
"compute_type",
"total_duration_s",
)
if key in render_log
}
asset = MediaAsset(
tenant_id=line.product.cad_file.tenant_id if (line.product and line.product.cad_file) else None,
order_line_id=line.id,
product_id=line.product_id,
asset_type=asset_type,
storage_key=storage_key,
mime_type=_resolve_output_mime_type(output_path),
file_size_bytes=output_file.stat().st_size if output_file.exists() else None,
width=None,
height=None,
render_config=render_config,
)
session.add(asset)
session.flush()
asset_id = str(asset.id)
else:
asset_id = str(existing_asset.id)
session.commit()
return OutputSaveResult(
status=status,
result_path=line.result_path,
asset_id=asset_id,
storage_key=storage_key,
asset_type=asset_type,
)
def prepare_order_line_render_context(
session: Session,
order_line_id: str,