chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31774 additions and 2753 deletions
+86 -15
View File
@@ -3,10 +3,12 @@ import uuid
from datetime import datetime, timedelta
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update as sql_update, func, case, distinct, and_, extract
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from app.database import get_db
from app.core.render_paths import resolve_result_path, result_path_to_storage_key
from app.models.user import User
from app.models.system_setting import SystemSetting
from app.models.cad_file import CadFile, ProcessingStatus
@@ -27,7 +29,7 @@ SETTINGS_DEFAULTS: dict[str, str] = {
"blender_eevee_samples": "64",
"thumbnail_format": "jpg",
"blender_smooth_angle": "30",
"cycles_device": "auto",
"cycles_device": "gpu",
"render_backend": "celery",
"blender_max_concurrent_renders": "3",
"product_thumbnail_priority": '["latest_render","cad_thumbnail"]',
@@ -63,7 +65,7 @@ class SettingsOut(BaseModel):
blender_eevee_samples: int = 64
thumbnail_format: str = "jpg"
blender_smooth_angle: int = 30
cycles_device: str = "auto"
cycles_device: str = "gpu"
render_backend: str = "celery"
blender_max_concurrent_renders: int = 3
product_thumbnail_priority: str = '["latest_render","cad_thumbnail"]'
@@ -225,9 +227,9 @@ def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
smtp_password=raw.get("smtp_password", ""),
smtp_from_address=raw.get("smtp_from_address", ""),
scene_linear_deflection=float(raw.get("scene_linear_deflection", "0.1")),
scene_angular_deflection=float(raw.get("scene_angular_deflection", "0.5")),
scene_angular_deflection=float(raw.get("scene_angular_deflection", "0.1")),
render_linear_deflection=float(raw.get("render_linear_deflection", "0.03")),
render_angular_deflection=float(raw.get("render_angular_deflection", "0.2")),
render_angular_deflection=float(raw.get("render_angular_deflection", "0.05")),
gltf_scale_factor=float(raw.get("gltf_scale_factor", "0.001")),
gltf_smooth_normals=raw.get("gltf_smooth_normals", "true") == "true",
viewer_max_distance=float(raw.get("viewer_max_distance", "50")),
@@ -680,7 +682,10 @@ async def seed_workflows(
):
"""Create the standard workflow definitions if they do not already exist."""
from app.domains.rendering.models import WorkflowDefinition
from app.domains.rendering.workflow_config_utils import build_preset_workflow_config
from app.domains.rendering.workflow_config_utils import (
build_preset_workflow_config,
build_workflow_blueprint_config,
)
STANDARD_WORKFLOWS = [
{
@@ -697,6 +702,13 @@ async def seed_workflows(
{"render_engine": "eevee", "samples": 64, "resolution": [1920, 1080]},
),
},
{
"name": "Still Image — Graph",
"config": build_preset_workflow_config(
"still_graph",
{"render_engine": "cycles", "samples": 256, "resolution": [1920, 1080]},
),
},
{
"name": "Turntable Animation",
"config": build_preset_workflow_config(
@@ -711,6 +723,18 @@ async def seed_workflows(
{"render_engine": "cycles", "samples": 128, "angles": [0, 45, 90]},
),
},
{
"name": "CAD Intake Blueprint",
"config": build_workflow_blueprint_config("cad_intake"),
},
{
"name": "Order Rendering Blueprint",
"config": build_workflow_blueprint_config("order_rendering"),
},
{
"name": "Still Graph Blueprint",
"config": build_workflow_blueprint_config("still_graph_reference"),
},
]
existing_result = await db.execute(select(WorkflowDefinition))
@@ -730,6 +754,57 @@ async def seed_workflows(
return {"created": created, "message": f"Created {created} workflow definition(s)"}
@router.post("/settings/backfill-workflows", status_code=status.HTTP_200_OK)
async def backfill_workflows(
admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db),
):
"""Rewrite persisted legacy workflow configs into canonical DAG form."""
from app.domains.rendering.models import WorkflowDefinition
from app.domains.rendering.workflow_config_utils import (
canonicalize_workflow_config,
workflow_config_requires_canonicalization,
)
from app.domains.rendering.workflow_schema import WorkflowConfig
result = await db.execute(select(WorkflowDefinition).order_by(WorkflowDefinition.created_at))
workflows = result.scalars().all()
updated: list[dict[str, str]] = []
invalid: list[dict[str, str]] = []
for workflow in workflows:
if not workflow_config_requires_canonicalization(workflow.config):
continue
try:
normalized = canonicalize_workflow_config(workflow.config)
WorkflowConfig.model_validate(normalized)
except (ValidationError, ValueError) as exc:
invalid.append(
{
"id": str(workflow.id),
"name": workflow.name,
"error": str(exc),
}
)
continue
workflow.config = normalized
flag_modified(workflow, "config")
updated.append({"id": str(workflow.id), "name": workflow.name})
await db.commit()
return {
"scanned": len(workflows),
"updated": len(updated),
"invalid": invalid,
"workflows": updated,
"message": f"Canonicalized {len(updated)} workflow definition(s)",
}
@router.get("/settings/renderer-status")
async def renderer_status(
admin: User = Depends(require_global_admin),
@@ -756,13 +831,10 @@ async def import_existing_media_assets(
created = 0
skipped = 0
from app.config import settings as _app_settings
def _normalize_key(path: str) -> str:
"""Strip UPLOAD_DIR prefix to store relative storage keys."""
key = str(path)
prefix = str(_app_settings.upload_dir).rstrip("/") + "/"
return key[len(prefix):] if key.startswith(prefix) else key
"""Normalize mixed legacy/canonical paths to a stable relative storage key."""
key = result_path_to_storage_key(path)
return key or str(path)
# 1. CadFiles with thumbnail_path
await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))
@@ -843,7 +915,6 @@ async def purge_render_media(
"""
import logging
from pathlib import Path
from app.config import settings
from app.core.storage import get_storage
from app.domains.media.models import MediaAsset, MediaAssetType
@@ -865,8 +936,8 @@ async def purge_render_media(
# Delete backing file
key = asset.storage_key
try:
candidate = Path(key) if Path(key).is_absolute() else Path(settings.upload_dir) / key
if candidate.exists():
candidate = resolve_result_path(key)
if candidate is not None and candidate.exists():
freed_bytes += candidate.stat().st_size
candidate.unlink()
deleted_files += 1