refactor(A2): replace blender-renderer HTTP service with render-worker Celery container

- Create render-worker/ with Dockerfile (Ubuntu + cadquery + Blender via host mount)
- Add render-worker/check_version.py: verifies Blender >= 5.0.1 at startup, Exit 1 on failure
- Add render-worker/scripts/: blender_render.py, still_render.py, turntable_render.py
- Create backend/app/services/render_blender.py: direct subprocess rendering
  - convert_step_to_stl() and export_per_part_stls() using cadquery
  - render_still(): STEP → STL → PNG via Blender subprocess
  - is_blender_available(): detects BLENDER_BIN env for render-worker context
- Create backend/app/domains/rendering/tasks.py: render_still_task + render_turntable_task
- Update step_processor.py: use subprocess path when BLENDER_BIN env is set (render-worker)
- Update step_tasks.py: generate_stl_cache uses direct cadquery instead of HTTP
- Remove blender-renderer and threejs-renderer from docker-compose.yml
- Replace worker-thumbnail with render-worker (Ubuntu + cadquery + Blender mount)
- Remove Docker SDK from backend Dockerfile (was only for flamenco scaling)
- Update .env.example: BLENDER_VERSION=5.0.1 documented
- Update celery_app.py: include domains.rendering.tasks in autodiscover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:48:46 +01:00
parent 1d6864fb64
commit 9d1a820295
16 changed files with 3118 additions and 108 deletions
+57 -37
View File
@@ -329,8 +329,9 @@ def _generate_thumbnail(
"height": 512,
})
elif renderer == "threejs":
size = int(settings["threejs_render_size"])
render_log.update({"width": size, "height": size})
# Three.js renderer removed in v2; treat as pillow fallback
renderer = "pillow"
render_log.update({"renderer": "pillow", "threejs_removed": True})
logger.info(f"Thumbnail renderer={renderer}, format={fmt}")
@@ -340,29 +341,25 @@ def _generate_thumbnail(
if renderer == "blender":
engine = settings["blender_engine"]
samples = int(settings[f"blender_{engine}_samples"])
extra = {
"engine": engine,
"samples": samples,
"stl_quality": settings["stl_quality"],
"smooth_angle": int(settings["blender_smooth_angle"]),
"cycles_device": settings["cycles_device"],
}
rendered_png, service_data = _render_via_service(
"http://blender-renderer:8100/render", step_path, tmp_png, extra
)
if not rendered_png:
logger.warning("Blender renderer failed; falling back to Pillow placeholder")
elif renderer == "threejs":
size = int(settings["threejs_render_size"])
extra2: dict = {"width": size, "height": size}
if part_colors is not None:
extra2["part_colors"] = part_colors
rendered_png, service_data = _render_via_service(
"http://threejs-renderer:8101/render", step_path, tmp_png, extra2
)
if not rendered_png:
logger.warning("Three.js renderer failed; falling back to Pillow placeholder")
from app.services.render_blender import is_blender_available, render_still
if is_blender_available():
try:
service_data = render_still(
step_path=step_path,
output_path=tmp_png,
engine=engine,
samples=samples,
stl_quality=settings["stl_quality"],
smooth_angle=int(settings["blender_smooth_angle"]),
cycles_device=settings["cycles_device"],
)
rendered_png = tmp_png if tmp_png.exists() else None
except Exception as exc:
logger.warning("Blender subprocess render failed: %s", exc)
rendered_png = None
else:
logger.warning("Blender not available in this container — falling back to Pillow placeholder")
# Merge rich service response data into render_log
if service_data:
@@ -669,20 +666,43 @@ def render_to_file(
extra["denoising_quality"] = denoising_quality
if denoising_use_gpu:
extra["denoising_use_gpu"] = denoising_use_gpu
rendered_png, service_data = _render_via_service(
"http://blender-renderer:8100/render", step, tmp_png, extra, job_id=job_id
)
from app.services.render_blender import is_blender_available, render_still
if is_blender_available():
try:
service_data = render_still(
step_path=step,
output_path=tmp_png,
engine=actual_engine,
samples=actual_samples,
stl_quality=settings["stl_quality"],
smooth_angle=int(settings["blender_smooth_angle"]),
cycles_device=actual_cycles_device,
width=w, height=h,
transparent_bg=transparent_bg,
part_colors=part_colors,
template_path=template_path,
target_collection=target_collection,
material_library_path=material_library_path,
material_map=material_map,
part_names_ordered=part_names_ordered,
lighting_only=lighting_only,
shadow_catcher=shadow_catcher,
rotation_x=rotation_x, rotation_y=rotation_y, rotation_z=rotation_z,
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,
)
rendered_png = tmp_png if tmp_png.exists() else None
except Exception as exc:
logger.warning("Blender subprocess render failed: %s", exc)
rendered_png = None
else:
logger.warning("Blender not available in this container — using Pillow fallback")
elif renderer == "threejs":
default_size = int(settings["threejs_render_size"])
w = width or default_size
h = height or default_size
render_log.update({"width": w, "height": h})
extra2: dict = {"width": w, "height": h}
if part_colors is not None:
extra2["part_colors"] = part_colors
rendered_png, service_data = _render_via_service(
"http://threejs-renderer:8101/render", step, tmp_png, extra2
)
# Three.js renderer removed in v2 — fall through to Pillow placeholder
logger.warning("Three.js renderer removed; using Pillow fallback")
if service_data:
for key in ("total_duration_s", "stl_duration_s", "render_duration_s",