247 lines
9.7 KiB
Bash
Executable File
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
|