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:
@@ -479,6 +479,7 @@ def render_order_line_task(self, order_line_id: str):
|
|||||||
focal_length_mm=focal_length_mm,
|
focal_length_mm=focal_length_mm,
|
||||||
sensor_width_mm=sensor_width_mm,
|
sensor_width_mm=sensor_width_mm,
|
||||||
material_override=override_mat,
|
material_override=override_mat,
|
||||||
|
log_callback=lambda line: emit(order_line_id, line),
|
||||||
)
|
)
|
||||||
success = True
|
success = True
|
||||||
render_log = {
|
render_log = {
|
||||||
|
|||||||
@@ -644,27 +644,47 @@ def render_cinematic_to_file(
|
|||||||
log_lines: list[str] = []
|
log_lines: list[str] = []
|
||||||
|
|
||||||
t_render = time.monotonic()
|
t_render = time.monotonic()
|
||||||
|
import selectors as _sel
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
text=True, env=env, start_new_session=True,
|
text=True, env=env, start_new_session=True,
|
||||||
)
|
)
|
||||||
try:
|
stderr_lines: list[str] = []
|
||||||
stdout, stderr = proc.communicate(timeout=7200) # 2hr max for cinematic (480 frames)
|
deadline = time.monotonic() + 7200 # 2hr max
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
try:
|
|
||||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
||||||
except (ProcessLookupError, OSError):
|
|
||||||
pass
|
|
||||||
stdout, stderr = proc.communicate()
|
|
||||||
|
|
||||||
for line in (stdout or "").splitlines():
|
sel = _sel.DefaultSelector()
|
||||||
logger.info("[cinematic] %s", line)
|
sel.register(proc.stdout, _sel.EVENT_READ, "stdout")
|
||||||
if "[cinematic_render]" in line:
|
sel.register(proc.stderr, _sel.EVENT_READ, "stderr")
|
||||||
log_lines.append(line)
|
|
||||||
if log_callback:
|
try:
|
||||||
log_callback(line)
|
while sel.get_map():
|
||||||
for line in (stderr or "").splitlines():
|
remaining = deadline - time.monotonic()
|
||||||
logger.warning("[cinematic stderr] %s", line)
|
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:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
Reference in New Issue
Block a user