#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: ./scripts/rerender_closed_legacy_still.sh Description: Re-renders a completed legacy still order line with the full legacy template, material, position, and USD context, then persists the canonical output and updates the linked media asset. EOF } if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then usage exit 0 fi ORDER_LINE_ID="${1:-}" if [ -z "$ORDER_LINE_ID" ]; then usage >&2 exit 1 fi REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" if [ -z "$REPO_ROOT" ]; then echo "This script must be run inside the repository." >&2 exit 1 fi cd "$REPO_ROOT" docker compose exec -T render-worker python3 - "$ORDER_LINE_ID" <<'PY' import json import re import sys from datetime import datetime from pathlib import Path import app.models.template # noqa: F401 from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, joinedload from app.config import settings from app.core.render_paths import resolve_result_path from app.domains.media.models import MediaAsset, MediaAssetType from app.domains.orders.models import OrderLine from app.domains.products.models import Product from app.domains.rendering.workflow_runtime_services import ( OrderLineRenderSetupResult, persist_order_line_output, resolve_order_line_template_context, resolve_render_position_context, ) from app.services.step_processor import build_part_colors, render_to_file def _sanitize(value: str) -> str: return re.sub(r"[^\w\-.]", "_", value.strip())[:100] or "output" order_line_id = sys.argv[1] engine = create_engine(settings.database_url.replace("+asyncpg", "")) with Session(engine) as session: line = session.execute( select(OrderLine) .where(OrderLine.id == order_line_id) .options( joinedload(OrderLine.product).joinedload(Product.cad_file), joinedload(OrderLine.output_type), ) ).scalar_one_or_none() if line is None: raise RuntimeError(f"Order line not found: {order_line_id}") if line.product is None or line.product.cad_file is None: raise RuntimeError(f"Order line {order_line_id} has no linked CAD file") if line.output_type is None: raise RuntimeError(f"Order line {order_line_id} has no output type") cad_file = line.product.cad_file render_settings = dict(line.output_type.render_settings or {}) if bool(render_settings.get("cinematic")): raise RuntimeError("This support script only handles still outputs, not cinematic outputs") if bool(getattr(line.output_type, "is_animation", False)): raise RuntimeError("This support script only handles still outputs, not animation outputs") materials_source = list(line.product.cad_part_materials or []) part_colors = {} parsed_names = [] if cad_file.parsed_objects: parsed_names = list(cad_file.parsed_objects.get("objects", []) or []) if materials_source: part_colors = build_part_colors(parsed_names, materials_source) usd_render_path = None usd_asset = session.execute( select(MediaAsset) .where( MediaAsset.cad_file_id == cad_file.id, MediaAsset.asset_type == MediaAssetType.usd_master, ) .order_by(MediaAsset.created_at.desc()) .limit(1) ).scalar_one_or_none() if usd_asset is not None: usd_render_path = resolve_result_path(usd_asset.storage_key) setup = OrderLineRenderSetupResult( status="ready", order_line=line, cad_file=cad_file, materials_source=materials_source, usd_render_path=usd_render_path, glb_reuse_path=None, part_colors=part_colors, render_start=datetime.utcnow(), ) template_context = resolve_order_line_template_context(session, setup) position_context = resolve_render_position_context(session, line) out_ext = "jpg" fmt = (line.output_type.output_format or "").lower() if fmt == "png": out_ext = "png" elif fmt in {"jpg", "jpeg"}: out_ext = "jpg" elif fmt == "webp": out_ext = "webp" render_width = int(render_settings["width"]) if render_settings.get("width") else None render_height = int(render_settings["height"]) if render_settings.get("height") else None render_engine = render_settings.get("engine") render_samples = int(render_settings["samples"]) if render_settings.get("samples") else None noise_threshold = str(render_settings.get("noise_threshold", "")) denoiser = str(render_settings.get("denoiser", "")) denoising_input_passes = str(render_settings.get("denoising_input_passes", "")) denoising_prefilter = str(render_settings.get("denoising_prefilter", "")) denoising_quality = str(render_settings.get("denoising_quality", "")) denoising_use_gpu = str(render_settings.get("denoising_use_gpu", "")) transparent_bg = bool(line.output_type.transparent_bg) render_overrides = getattr(line, "render_overrides", None) if isinstance(render_overrides, dict): if "width" in render_overrides: render_width = int(render_overrides["width"]) if "height" in render_overrides: render_height = int(render_overrides["height"]) if "samples" in render_overrides: render_samples = int(render_overrides["samples"]) if "engine" in render_overrides: render_engine = render_overrides["engine"] if "noise_threshold" in render_overrides: noise_threshold = str(render_overrides["noise_threshold"]) if "denoiser" in render_overrides: denoiser = str(render_overrides["denoiser"]) if "denoising_input_passes" in render_overrides: denoising_input_passes = str(render_overrides["denoising_input_passes"]) if "denoising_prefilter" in render_overrides: denoising_prefilter = str(render_overrides["denoising_prefilter"]) if "denoising_quality" in render_overrides: denoising_quality = str(render_overrides["denoising_quality"]) if "denoising_use_gpu" in render_overrides: denoising_use_gpu = str(render_overrides["denoising_use_gpu"]) if "transparent_bg" in render_overrides: transparent_bg = bool(render_overrides["transparent_bg"]) if "output_format" in render_overrides: fmt_override = str(render_overrides["output_format"]).lower() if fmt_override == "png": out_ext = "png" elif fmt_override in {"jpg", "jpeg"}: out_ext = "jpg" elif fmt_override == "webp": out_ext = "webp" product_name = line.product.name or getattr(line.product, "pim_id", None) or "product" output_type_name = line.output_type.name or "render" filename = f"{_sanitize(product_name)}_{_sanitize(output_type_name)}.{out_ext}" output_dir = Path(settings.upload_dir) / "renders" / str(line.id) output_dir.mkdir(parents=True, exist_ok=True) output_path = str(output_dir / filename) success, render_log = render_to_file( step_path=cad_file.stored_path, output_path=output_path, part_colors=part_colors, width=render_width, height=render_height, transparent_bg=transparent_bg, engine=render_engine, samples=render_samples, template_path=template_context.template.blend_file_path if template_context.template else None, target_collection=template_context.template.target_collection if template_context.template else "Product", material_library_path=template_context.material_library if template_context.use_materials else None, material_map=template_context.material_map, part_names_ordered=parsed_names or None, lighting_only=bool(template_context.template.lighting_only) if template_context.template else False, shadow_catcher=bool(template_context.template.shadow_catcher_enabled) if template_context.template else False, cycles_device=line.output_type.cycles_device, rotation_x=position_context.rotation_x, rotation_y=position_context.rotation_y, rotation_z=position_context.rotation_z, focal_length_mm=position_context.focal_length_mm, sensor_width_mm=position_context.sensor_width_mm, material_override=template_context.override_material, job_id=str(line.id), order_line_id=str(line.id), noise_threshold=noise_threshold, denoiser=denoiser, denoising_input_passes=denoising_input_passes, denoising_prefilter=denoising_prefilter, denoising_quality=denoising_quality, denoising_use_gpu=denoising_use_gpu, usd_path=usd_render_path, ) if not success: raise RuntimeError(json.dumps(render_log, ensure_ascii=True)) persisted = persist_order_line_output( session, line, success=True, output_path=output_path, render_log=render_log if isinstance(render_log, dict) else None, render_completed_at=datetime.utcnow(), ) print( json.dumps( { "order_line_id": str(line.id), "result_path": persisted.result_path, "asset_id": persisted.asset_id, "storage_key": persisted.storage_key, "asset_type": persisted.asset_type.value if persisted.asset_type else None, "template": template_context.template.name if template_context.template else None, "material_map_count": len(template_context.material_map or {}), "usd_path": str(usd_render_path) if usd_render_path else None, "duration_s": render_log.get("total_duration_s") if isinstance(render_log, dict) else None, }, ensure_ascii=True, ) ) PY