EasyOCR GPU crash隔离:子进程隔离GPU推理,崩溃只杀子进程不冻电脑;新增easyocr_worker.py常驻worker;主进程降级到Windows OCR
This commit is contained in:
+103
-31
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user