Files
HartOMat/scripts/rerender_closed_legacy_still.sh

247 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/rerender_closed_legacy_still.sh <order-line-id>
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