feat: per-line render overrides — override any output type setting at order time
Instead of duplicating output types for every variation (WebP vs PNG,
different resolution), keep one canonical output type and override
specific fields per order line via render_overrides JSONB.
Backend:
- render_overrides JSONB column on OrderLine (DB migration)
- Render task merges overrides with output type settings (format, width,
height, samples, engine, denoiser, transparent_bg, cycles_device)
- POST /orders/{id}/batch-render-overrides endpoint for bulk override
- PatchLineBody accepts render_overrides for per-line patching
Frontend:
- Batch render overrides section on OrderDetail: output format dropdown
(PNG/JPG/WebP) + resolution dropdown (512-4096)
- Clear button to remove overrides
MCP:
- create_order tool: accepts product_ids, output_type, render_overrides,
material_override — enables "render all products as WebP" via Claude
- set_render_overrides tool: batch override on existing orders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,7 @@ class OrderLine(Base):
|
||||
nullable=True,
|
||||
)
|
||||
material_override: Mapped[str | None] = mapped_column(String(200), nullable=True, default=None)
|
||||
render_overrides: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True
|
||||
|
||||
@@ -67,6 +67,7 @@ class OrderLineCreate(BaseModel):
|
||||
global_render_position_id: uuid.UUID | None = None
|
||||
gewuenschte_bildnummer: str | None = None
|
||||
material_override: str | None = None
|
||||
render_overrides: dict | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
@@ -93,6 +94,7 @@ class OrderLineOut(BaseModel):
|
||||
render_started_at: datetime | None = None
|
||||
render_completed_at: datetime | None = None
|
||||
material_override: str | None = None
|
||||
render_overrides: dict | None = None
|
||||
notes: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -55,6 +55,14 @@ def dispatch_order_line_render(order_line_id: str):
|
||||
w = int(rs.get("width", 0) or 0)
|
||||
h = int(rs.get("height", 0) or 0)
|
||||
max_dim = max(w, h)
|
||||
# Apply render_overrides for routing decisions
|
||||
ro = getattr(line, 'render_overrides', None) or {}
|
||||
if ro.get("width"):
|
||||
w = int(ro["width"])
|
||||
if ro.get("height"):
|
||||
h = int(ro["height"])
|
||||
if w or h:
|
||||
max_dim = max(w, h)
|
||||
|
||||
if max_dim > 0 and max_dim <= 1024 and not is_animation:
|
||||
target_queue = "asset_pipeline_light"
|
||||
@@ -391,6 +399,56 @@ def render_order_line_task(self, order_line_id: str):
|
||||
transparent_bg = bool(line.output_type and line.output_type.transparent_bg)
|
||||
cycles_device_val = (line.output_type.cycles_device or "auto") if line.output_type else "auto"
|
||||
|
||||
# Apply per-line render overrides (format, resolution, samples, etc.)
|
||||
_render_overrides = getattr(line, 'render_overrides', None)
|
||||
if _render_overrides and 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 'frame_count' in _render_overrides:
|
||||
frame_count = int(_render_overrides['frame_count'])
|
||||
if 'fps' in _render_overrides:
|
||||
fps = int(_render_overrides['fps'])
|
||||
if 'bg_color' in _render_overrides:
|
||||
bg_color = _render_overrides['bg_color']
|
||||
if 'turntable_axis' in _render_overrides:
|
||||
turntable_axis = _render_overrides['turntable_axis']
|
||||
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 'cycles_device' in _render_overrides:
|
||||
cycles_device_val = _render_overrides['cycles_device']
|
||||
emit(order_line_id, f"Render overrides active: {_render_overrides}")
|
||||
|
||||
# Apply output_format override (affects out_ext and filename)
|
||||
if 'output_format' in _render_overrides:
|
||||
fmt_override = _render_overrides['output_format'].lower()
|
||||
if fmt_override == "mp4":
|
||||
out_ext = "mp4"
|
||||
elif fmt_override == "webp":
|
||||
out_ext = "webp"
|
||||
elif fmt_override in ("png", "jpg", "jpeg"):
|
||||
out_ext = "png" if fmt_override == "png" else "jpg"
|
||||
# Rebuild filename with new extension
|
||||
filename = f"{_sanitize(product_name)}_{_sanitize(ot_name)}.{out_ext}"
|
||||
output_path = str(render_dir / filename)
|
||||
|
||||
# Build ordered part names list for index-based Blender matching
|
||||
part_names_ordered = None
|
||||
if cad_file and cad_file.parsed_objects:
|
||||
|
||||
Reference in New Issue
Block a user