EasyOCR GPU crash隔离:子进程隔离GPU推理,崩溃只杀子进程不冻电脑;新增easyocr_worker.py常驻worker;主进程降级到Windows OCR

This commit is contained in:
youpaishidifu
2026-07-28 19:24:33 +08:00
parent 94cf619f3d
commit 7102821127
2 changed files with 195 additions and 31 deletions
+103 -31
View File
@@ -37,6 +37,8 @@ import win32process
import re
import ctypes
import difflib
import subprocess
import json
import psutil
import os
from datetime import datetime
@@ -182,6 +184,9 @@ class MessageMonitor:
self._ocr_engine = None
# EasyOCR 备用引擎(lazy init)
self._easyocr_reader = None
# EasyOCR 子进程(GPU crash 隔离,崩溃只杀子进程不冻电脑)
self._easyocr_proc: Optional['subprocess.Popen'] = None
self._easyocr_broken: bool = False
# 引擎选择计数器
self._ocr_engine_wins: dict = {"windows": 0, "easyocr": 0}
# 上次 OCR 原始文本(用于增量检测)
@@ -208,11 +213,7 @@ class MessageMonitor:
if self._running:
return
self._running = True
# 在主线程预加载 EasyOCR,避免 daemon 线程 torch 段错误
if _HAS_EASYOCR:
import torch
torch.set_num_threads(1)
self._init_easyocr()
# EasyOCR 已在子进程隔离(easyocr_worker.py),不需在主线程预加载
self._thread = threading.Thread(target=self._run_loop, daemon=True, name="monitor")
self._thread.start()
logger.info("消息监控已启动")
@@ -766,6 +767,82 @@ class MessageMonitor:
logger.error(f"[ocr] EasyOCR 初始化失败: {e}")
self._easyocr_reader = None
# ── EasyOCR 子进程隔离 ────────────────────────────────────
# 直接 GPU 推理 crash(access violation)会杀整个进程 + 冻电脑。
# 子进程方案:GPU crash 只杀子进程,主进程降级到 Windows OCR。
def _ensure_easyocr_subprocess(self) -> bool:
"""确保 EasyOCR 子进程在运行。返回 True = 可用。"""
if self._easyocr_broken:
return False
if self._easyocr_proc is not None:
if self._easyocr_proc.poll() is None:
return True # 还在跑
logger.warning("[ocr] EasyOCR 子进程已退出,重启...")
self._easyocr_proc = None
worker_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'easyocr_worker.py'
)
if not os.path.exists(worker_path):
logger.error(f"[ocr] EasyOCR worker 不存在: {worker_path}")
self._easyocr_broken = True
return False
try:
self._easyocr_proc = subprocess.Popen(
[sys.executable, '-u', worker_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, # 防 stderr 管道满阻塞
text=True,
bufsize=1, # 行缓冲
)
logger.debug("[ocr] EasyOCR 子进程已启动")
return True
except Exception as e:
logger.error(f"[ocr] 启动 EasyOCR 子进程失败: {e}")
self._easyocr_broken = True
self._easyocr_proc = None
return False
def _call_easyocr_safe(self, img_np) -> Optional[str]:
"""通过子进程安全调用 EasyOCR。返回识别文本,失败/崩溃时返回 None。"""
import io
import base64
if not self._ensure_easyocr_subprocess():
return None
try:
# numpy → PIL → PNG bytes → base64
img_pil = Image.fromarray(img_np)
buf = io.BytesIO()
img_pil.save(buf, format='PNG')
img_b64 = base64.b64encode(buf.getvalue()).decode()
self._easyocr_proc.stdin.write(json.dumps({"img": img_b64}) + "\n")
self._easyocr_proc.stdin.flush()
line = self._easyocr_proc.stdout.readline()
if not line:
raise ConnectionError("EasyOCR 子进程 stdout 关闭")
result = json.loads(line)
if result.get("ok"):
return result["text"]
else:
logger.debug(f"[ocr] EasyOCR worker 错误: {result.get('error', '?')[:100]}")
return None
except (BrokenPipeError, ConnectionError, OSError) as e:
logger.debug(f"[ocr] EasyOCR 子进程通信断开: {e}")
self._easyocr_broken = True
self._easyocr_proc = None
return None
except Exception as e:
logger.debug(f"[ocr] EasyOCR 子进程异常: {e}")
return None
@staticmethod
def _score_ocr_text(text: str) -> float:
"""为 OCR 结果质量打分(越高越好)。对比引擎用。"""
@@ -924,21 +1001,18 @@ class MessageMonitor:
win_text = win_result.text.strip()
logger.debug(f"[ocr] Windows OCR 完成: {len(win_text)} chars")
# EasyOCR 备用
# EasyOCR 备用(GPU crash 隔离:通过子进程调用)
easy_text = ""
if _HAS_EASYOCR:
logger.debug("[ocr] EasyOCR 检查/初始化...")
self._init_easyocr()
logger.debug("[ocr] EasyOCR reader 就绪状态")
if self._easyocr_reader:
try:
logger.debug("[ocr] EasyOCR 识别开始...")
img_np = _np.array(im)
easy_results = self._easyocr_reader.readtext(img_np)
easy_text = ' '.join([r[1] for r in easy_results]).strip()
logger.debug(f"[ocr] EasyOCR 识别完成: {len(easy_text)} chars")
except Exception as e:
logger.debug(f"[ocr] EasyOCR 识别失败: {e}")
if _HAS_EASYOCR and not self._easyocr_broken:
logger.debug("[ocr] EasyOCR 子进程...")
img_np = _np.array(im)
easy_text = self._call_easyocr_safe(img_np)
if easy_text is None:
self._easyocr_broken = True
easy_text = ""
logger.warning("[ocr] EasyOCR 子进程崩溃,已禁用(本次会话不再尝试)")
elif easy_text:
logger.debug(f"[ocr] EasyOCR 识别完成: {len(easy_text)} chars")
# 引擎原始输出逐行标记日志(用于长期对比两引擎能力)
if win_text:
@@ -1041,18 +1115,16 @@ class MessageMonitor:
logger.debug(f"[wechat] Windows OCR 完成: {len(win_text)} chars")
easy_text = ""
if _HAS_EASYOCR:
logger.debug("[wechat] EasyOCR 检查/初始化...")
self._init_easyocr()
logger.debug("[wechat] EasyOCR reader 就绪状态")
if self._easyocr_reader:
try:
logger.debug("[wechat] EasyOCR 识别开始...")
img_np = _np.array(im)
easy_results = self._easyocr_reader.readtext(img_np)
easy_text = ' '.join([r[1] for r in easy_results]).strip()
except Exception as e:
logger.debug(f"[wechat] EasyOCR 识别失败: {e}")
if _HAS_EASYOCR and not self._easyocr_broken:
logger.debug("[wechat] EasyOCR 子进程...")
img_np = _np.array(im)
easy_text = self._call_easyocr_safe(img_np)
if easy_text is None:
self._easyocr_broken = True
easy_text = ""
logger.warning("[wechat] EasyOCR 子进程崩溃,已禁用(本次会话不再尝试)")
elif easy_text:
logger.debug(f"[wechat] EasyOCR 识别完成: {len(easy_text)} chars")
# 引擎原始输出逐行标记日志(用于长期对比两引擎能力)
if win_text: