chore: snapshot workflow migration progress
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user