feat: live frame progress streaming for cinematic renders

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) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 21:39:41 +01:00
parent e26d76154b
commit caffe7809c
2 changed files with 37 additions and 16 deletions
@@ -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 = {
+36 -16
View File
@@ -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(