From caffe7809c66eee4df134428f017a7ed0b9b94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 15 Mar 2026 21:39:41 +0100 Subject: [PATCH] feat: live frame progress streaming for cinematic renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced communicate() (blocking) with selectors-based line-by-line stdout streaming — same pattern as still render. Each frame now streams live to the frontend: [cinematic_render] Frame 42/480 -- 55.3s elapsed (0.76 fps) Pipeline: Blender stdout → log_callback → emit() → Redis → LiveRenderLog poll (2s) → frontend display Also added log_callback parameter to cinematic render task call. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pipeline/tasks/render_order_line.py | 1 + backend/app/services/render_blender.py | 52 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/backend/app/domains/pipeline/tasks/render_order_line.py b/backend/app/domains/pipeline/tasks/render_order_line.py index 4b80b16..148835a 100644 --- a/backend/app/domains/pipeline/tasks/render_order_line.py +++ b/backend/app/domains/pipeline/tasks/render_order_line.py @@ -479,6 +479,7 @@ def render_order_line_task(self, order_line_id: str): focal_length_mm=focal_length_mm, sensor_width_mm=sensor_width_mm, material_override=override_mat, + log_callback=lambda line: emit(order_line_id, line), ) success = True render_log = { diff --git a/backend/app/services/render_blender.py b/backend/app/services/render_blender.py index 50ae7f5..d24bc2f 100644 --- a/backend/app/services/render_blender.py +++ b/backend/app/services/render_blender.py @@ -644,27 +644,47 @@ def render_cinematic_to_file( log_lines: list[str] = [] t_render = time.monotonic() + import selectors as _sel proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, start_new_session=True, ) - try: - stdout, stderr = proc.communicate(timeout=7200) # 2hr max for cinematic (480 frames) - except subprocess.TimeoutExpired: - try: - os.killpg(os.getpgid(proc.pid), signal.SIGTERM) - except (ProcessLookupError, OSError): - pass - stdout, stderr = proc.communicate() + stderr_lines: list[str] = [] + deadline = time.monotonic() + 7200 # 2hr max - for line in (stdout or "").splitlines(): - logger.info("[cinematic] %s", line) - if "[cinematic_render]" in line: - log_lines.append(line) - if log_callback: - log_callback(line) - for line in (stderr or "").splitlines(): - logger.warning("[cinematic stderr] %s", line) + sel = _sel.DefaultSelector() + sel.register(proc.stdout, _sel.EVENT_READ, "stdout") + sel.register(proc.stderr, _sel.EVENT_READ, "stderr") + + try: + while sel.get_map(): + remaining = deadline - time.monotonic() + if remaining <= 0: + try: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + except (ProcessLookupError, OSError): + pass + break + events = sel.select(timeout=min(remaining, 2.0)) + for key, _ in events: + line = key.fileobj.readline() + if not line: + sel.unregister(key.fileobj) + continue + line = line.rstrip("\n") + if key.data == "stdout": + logger.info("[cinematic] %s", line) + if "[cinematic_render]" in line: + log_lines.append(line) + if log_callback: + log_callback(line) + else: + stderr_lines.append(line) + logger.warning("[cinematic stderr] %s", line) + finally: + sel.close() + + proc.wait() if proc.returncode != 0: raise RuntimeError(