fix: restore-photo colorize hang and AVIF decode failures

- Fix dispatcher pipe deadlock: drain stdout pipe in a background thread
  to prevent blocking when ONNX runtime output exceeds 64KB pipe buffer
- Add 5-minute SSE stall timeout so the UI shows an error instead of
  hanging forever when async AI processing stalls
- Guard CPU colorization: skip for images >2MP on CPU and when DDColor
  model is not installed, with clear user-facing messages
- Add AVIF decode fallback via ImageMagick for bitstream variants that
  Sharp's bundled libheif cannot decode (affects all tools)
This commit is contained in:
SnapOtter
2026-05-13 15:42:24 +08:00
parent 917c1ff773
commit 3760885342
6 changed files with 143 additions and 18 deletions
+22 -5
View File
@@ -118,7 +118,12 @@ def _run_script_main(script_name, args):
Since some scripts (like remove_bg.py) manipulate file descriptors directly
(os.dup2), we use a pipe at the fd level rather than StringIO.
A drain thread reads the pipe concurrently to prevent deadlock when
scripts produce more than 64 KB of stdout (e.g. ONNX runtime logging).
"""
import threading
script_dir = os.path.dirname(os.path.abspath(__file__))
# ── Feature gate: reject scripts whose bundle is not installed ──
@@ -150,6 +155,20 @@ def _run_script_main(script_name, args):
old_sys_stdout = sys.stdout
sys.stdout = os.fdopen(1, "w", closefd=False)
# Drain the pipe in a background thread so the pipe buffer never fills.
captured_chunks = []
def _drain():
with os.fdopen(read_fd, "r") as f:
while True:
chunk = f.read(8192)
if not chunk:
break
captured_chunks.append(chunk)
drain_thread = threading.Thread(target=_drain, daemon=True)
drain_thread.start()
exit_code = 0
try:
sys.argv = ["script.py"] + args
@@ -178,7 +197,7 @@ def _run_script_main(script_name, args):
# Flush before restoring
sys.stdout.flush()
# Restore stdout fd
# Restore stdout fd (closes the pipe write end, unblocking the drain thread)
os.dup2(real_stdout_fd, 1)
os.close(real_stdout_fd)
@@ -188,10 +207,8 @@ def _run_script_main(script_name, args):
# Restore sys.argv
sys.argv = old_argv
# Read captured output from the pipe
read_file = os.fdopen(read_fd, "r")
captured = read_file.read()
read_file.close()
drain_thread.join(timeout=10)
captured = "".join(captured_chunks)
return captured.strip(), exit_code
+20 -10
View File
@@ -625,16 +625,26 @@ def main():
# ── Step 5: Colorization ─────────────────────────────────
colorized = False
if do_colorize and bw_detected:
emit_progress(82, "Colorizing B&W photo")
try:
result, colorized = colorize_bw(result, intensity=0.85)
if colorized:
steps_applied.append("colorize")
emit_progress(92, "Colorization complete")
else:
emit_progress(92, "Colorization model not available")
except Exception as e:
emit_progress(92, f"Colorization skipped: {str(e)[:40]}")
total_pixels = orig_h * orig_w
has_gpu = device == "cuda"
max_pixels = 8_000_000 if has_gpu else 2_000_000
if total_pixels > max_pixels and not has_gpu:
mp = total_pixels / 1_000_000
emit_progress(92, f"Colorization skipped: image too large for CPU ({mp:.1f}MP, max 2MP)")
elif not os.path.exists(DDCOLOR_MODEL_PATH):
emit_progress(92, "Colorization skipped: DDColor model not installed")
else:
emit_progress(82, "Colorizing B&W photo")
try:
result, colorized = colorize_bw(result, intensity=0.85)
if colorized:
steps_applied.append("colorize")
emit_progress(92, "Colorization complete")
else:
emit_progress(92, "Colorization model not available")
except Exception as e:
emit_progress(92, f"Colorization skipped: {str(e)[:40]}")
else:
emit_progress(92, "Colorization skipped")