修复EasyOCR CUDA OOM冻结+时间戳day=0崩溃+OCR图片分目录img/raw+processed
- 冻结根因: CUDA OOM后worker不退出, 损坏context持续占显存, 父进程不杀子进程 反复发图→线程堆积+显存耗尽→桌面冻结 - worker: readtext CUDA/kernel错误→立即退出释放显存(faulerhandler已有兜底) - 父进程: 收到CUDA错误→立即杀子进程+30s冷却; 连续2次CUDA错误→EASYOCR_FORCE_CPU=1 强制CPU模式启动(显存不足降级防冻结) - 时间戳day=0崩溃: _is_timestamp_old跨午夜用replace(day-1), 每月1号day=1→day=0 抛ValueError day is out of range for month→微信OCR整轮失败; 改用timedelta(days=±1) - OCR调试图片回源码目录: img/raw(原图PNG) + img/processed(处理后JPG) 分目录存储 - 崩溃日志回源码根 easyocr_crash.log (不写%LOCALAPPDATA%) - _clean_old_ocr_debug 默认 keep=20 (回退上一轮激进keep=6) - .gitignore: 忽略 img/ 与 worker_stderr/stdout.txt
This commit is contained in:
+59
-32
@@ -93,7 +93,7 @@ import json
|
||||
import psutil
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import signal
|
||||
from PIL import Image, ImageGrab, ImageEnhance, ImageFilter
|
||||
from winrt.windows.media.ocr import OcrEngine
|
||||
@@ -257,6 +257,8 @@ class MessageMonitor:
|
||||
# EasyOCR 连续失败计数:前2次保留子进程(预留初始化时间),第3次杀进程重来
|
||||
self._easyocr_consecutive_failures: int = 0
|
||||
self._easyocr_max_failures: int = 3 # 连续3次失败后杀进程
|
||||
# EasyOCR CUDA 错误计数:≥2 次 OOM/kernel error → 下次启动强制 CPU(防冻结)
|
||||
self._easyocr_cuda_oom_count: int = 0
|
||||
# 引擎选择计数器
|
||||
self._ocr_engine_wins: dict = {"windows": 0, "easyocr": 0}
|
||||
# 上次 OCR 原始文本(用于增量检测)
|
||||
@@ -307,6 +309,7 @@ class MessageMonitor:
|
||||
self._easyocr_broken = False
|
||||
self._easyocr_retry_until = 0.0 # 清除崩溃冷却
|
||||
self._easyocr_call_count = 0
|
||||
self._easyocr_cuda_oom_count = 0 # 重启后重新评估 GPU/CPU
|
||||
self._easyocr_proc = None # 确保 clean state
|
||||
# EasyOCR 已在子进程隔离(easyocr_worker.py),不需在主线程预加载
|
||||
# ⭐ 预热子进程:后台启动 + 60s 初始化握手,主循环不阻塞
|
||||
@@ -355,6 +358,7 @@ class MessageMonitor:
|
||||
self._easyocr_broken = False
|
||||
self._easyocr_retry_until = 0.0 # 清除崩溃冷却
|
||||
self._easyocr_call_count = 0
|
||||
self._easyocr_cuda_oom_count = 0 # 重启后重新评估 GPU/CPU
|
||||
|
||||
# 释放钩子回调引用(防止 GC 延迟导致卸载后崩溃)
|
||||
if hasattr(self, '_esc_hook_proc'):
|
||||
@@ -1072,6 +1076,11 @@ class MessageMonitor:
|
||||
return False
|
||||
|
||||
try:
|
||||
_worker_env = dict(os.environ)
|
||||
if self._easyocr_cuda_oom_count >= 2:
|
||||
# ⭐ 连续 CUDA OOM/kernel error → 强制 CPU 模式启动(显存不足时防冻结)
|
||||
_worker_env["EASYOCR_FORCE_CPU"] = "1"
|
||||
logger.warning("[ocr] EasyOCR 连续CUDA错误≥2次,本次强制 CPU 模式启动 worker")
|
||||
self._easyocr_proc = subprocess.Popen(
|
||||
[sys.executable, '-u', worker_path],
|
||||
stdin=subprocess.PIPE,
|
||||
@@ -1079,6 +1088,7 @@ class MessageMonitor:
|
||||
stderr=subprocess.PIPE, # 捕获 worker 错误日志(防崩溃排查)
|
||||
text=True,
|
||||
bufsize=1, # 行缓冲
|
||||
env=_worker_env,
|
||||
)
|
||||
logger.debug("[ocr] EasyOCR 子进程已启动,等待初始化握手...")
|
||||
except Exception as e:
|
||||
@@ -1228,7 +1238,23 @@ class MessageMonitor:
|
||||
self._easyocr_call_count = 0
|
||||
return result["text"]
|
||||
else:
|
||||
logger.debug(f"[ocr] EasyOCR worker 错误: {result.get('error', '?')[:100]}")
|
||||
err = result.get('error', '?')[:100]
|
||||
logger.debug(f"[ocr] EasyOCR worker 错误: {err}")
|
||||
# ⭐ CUDA OOM / kernel error:worker 即将自杀退出(context 已损坏),
|
||||
# 这里立即确认杀掉 + 冷却,避免后续轮询继续往坏进程发图
|
||||
if 'cuda' in err.lower() or 'out of memory' in err.lower():
|
||||
self._easyocr_cuda_oom_count += 1
|
||||
logger.warning(
|
||||
f"[ocr] EasyOCR CUDA 错误(累计{self._easyocr_cuda_oom_count}次),"
|
||||
f"杀子进程+冷却,{('下次强制CPU' if self._easyocr_cuda_oom_count >= 2 else '保持GPU')}")
|
||||
try:
|
||||
if self._easyocr_proc and self._easyocr_proc.poll() is None:
|
||||
self._easyocr_proc.kill()
|
||||
self._easyocr_proc.wait(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
self._easyocr_proc = None
|
||||
self._easyocr_retry_until = time.time() + 30
|
||||
return None
|
||||
except (BrokenPipeError, ConnectionError, OSError) as e:
|
||||
logger.debug(f"[ocr] EasyOCR 子进程通信断开: {e},30s 后自动重试")
|
||||
@@ -1506,11 +1532,11 @@ class MessageMonitor:
|
||||
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
|
||||
_dbg_path = ""
|
||||
try:
|
||||
debug_dir = _get_ocr_debug_dir()
|
||||
debug_dir = _get_ocr_raw_dir()
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_dbg_path = os.path.join(debug_dir, f"ocr_debug_qq_{ts}.png")
|
||||
im.save(_dbg_path, format="PNG")
|
||||
_clean_old_ocr_debug(debug_dir, keep=6, prefix="ocr_debug_qq_")
|
||||
_clean_old_ocr_debug(debug_dir, prefix="ocr_debug_qq_")
|
||||
if os.path.exists(_dbg_path):
|
||||
logger.info(f"[qq] 调试截图已保存: {_dbg_path}")
|
||||
else:
|
||||
@@ -1549,7 +1575,7 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] 预处理: resize 2x + sharpen(1.2) + contrast(1.2)")
|
||||
# 保存OCR处理后的图片(JPG格式)用于肉眼对比
|
||||
try:
|
||||
debug_dir = _get_ocr_debug_dir()
|
||||
debug_dir = _get_ocr_processed_dir()
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_processed_path = os.path.join(debug_dir, f"ocr_processed_qq_{ts}.jpg")
|
||||
# 转换为RGB模式保存JPG(不支持RGBA)
|
||||
@@ -1558,7 +1584,7 @@ class MessageMonitor:
|
||||
else:
|
||||
im_rgb = im
|
||||
im_rgb.save(_processed_path, format="JPEG", quality=95)
|
||||
_clean_old_ocr_debug(debug_dir, keep=6, prefix="ocr_processed_qq_")
|
||||
_clean_old_ocr_debug(debug_dir, prefix="ocr_processed_qq_")
|
||||
if os.path.exists(_processed_path):
|
||||
logger.debug(f"[qq] OCR处理后图片已保存: {_processed_path}")
|
||||
except Exception as e:
|
||||
@@ -1639,11 +1665,11 @@ class MessageMonitor:
|
||||
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
|
||||
_dbg_path = ""
|
||||
try:
|
||||
debug_dir = _get_ocr_debug_dir()
|
||||
debug_dir = _get_ocr_raw_dir()
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_dbg_path = os.path.join(debug_dir, f"ocr_debug_wx_{ts}.png")
|
||||
im.save(_dbg_path, format="PNG")
|
||||
_clean_old_ocr_debug(debug_dir, keep=6, prefix="ocr_debug_wx_")
|
||||
_clean_old_ocr_debug(debug_dir, prefix="ocr_debug_wx_")
|
||||
if os.path.exists(_dbg_path):
|
||||
logger.info(f"[wechat] 调试截图已保存: {_dbg_path}")
|
||||
else:
|
||||
@@ -1680,7 +1706,7 @@ class MessageMonitor:
|
||||
im = ImageEnhance.Contrast(im).enhance(1.2) # 降低对比度,保持笔画连贯
|
||||
# 保存OCR处理后的图片(JPG格式)用于肉眼对比
|
||||
try:
|
||||
debug_dir = _get_ocr_debug_dir()
|
||||
debug_dir = _get_ocr_processed_dir()
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_processed_path = os.path.join(debug_dir, f"ocr_processed_wx_{ts}.jpg")
|
||||
# 转换为RGB模式保存JPG(不支持RGBA)
|
||||
@@ -1689,7 +1715,7 @@ class MessageMonitor:
|
||||
else:
|
||||
im_rgb = im
|
||||
im_rgb.save(_processed_path, format="JPEG", quality=95)
|
||||
_clean_old_ocr_debug(debug_dir, keep=6, prefix="ocr_processed_wx_")
|
||||
_clean_old_ocr_debug(debug_dir, prefix="ocr_processed_wx_")
|
||||
if os.path.exists(_processed_path):
|
||||
logger.debug(f"[wechat] OCR处理后图片已保存: {_processed_path}")
|
||||
except Exception as e:
|
||||
@@ -2954,12 +2980,14 @@ class MessageMonitor:
|
||||
return False
|
||||
diff = (now - parsed).total_seconds()
|
||||
# 跨午夜: diff > 12h 说明时间戳来自昨天
|
||||
# ⚠ 用 timedelta 回退而非 replace(day-1):每月1号 day=1 时 day-1=0 抛
|
||||
# ValueError("day is out of range for month") → 微信 OCR 整轮失败
|
||||
if diff > 43200:
|
||||
parsed = parsed.replace(day=parsed.day - 1)
|
||||
parsed = parsed - timedelta(days=1)
|
||||
diff = (now - parsed).total_seconds()
|
||||
# 未来时间戳: diff < -12h 说明是明天(当前 23:55,时间戳 00:05)
|
||||
if diff < -43200:
|
||||
parsed = parsed.replace(day=parsed.day + 1)
|
||||
parsed = parsed + timedelta(days=1)
|
||||
diff = (now - parsed).total_seconds()
|
||||
# OCR 误读兜底:h<12 且判定为老 → 尝试 h+10 / h+12(OCR 丢失前导1:15→5)
|
||||
if diff > max_age and h < 12:
|
||||
@@ -4246,40 +4274,39 @@ class MessageMonitor:
|
||||
self._cached_hwnd.pop(platform, None)
|
||||
self._cached_pid.pop(platform, None)
|
||||
|
||||
def _get_ocr_debug_dir() -> str:
|
||||
"""返回 OCR 调试截图保存目录。
|
||||
def _get_ocr_raw_dir() -> str:
|
||||
"""OCR 原始截图(PNG)保存目录:源码目录下的 img/raw。
|
||||
|
||||
放在用户本地数据目录(%LOCALAPPDATA%/windowmsgbot/ocr_debug),
|
||||
避免在源码目录频繁创建/删除图片被安全软件误报为勒索行为。
|
||||
统一存源码目录(历史版本即如此,从未被 360 误报),并按 raw / processed
|
||||
分目录存放,原图与处理后图片互不混杂,便于排查。
|
||||
"""
|
||||
try:
|
||||
local_appdata = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
|
||||
if local_appdata:
|
||||
debug_dir = os.path.join(local_appdata, "windowmsgbot", "ocr_debug")
|
||||
else:
|
||||
debug_dir = os.path.join(os.path.expanduser("~"), "windowmsgbot", "ocr_debug")
|
||||
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img", "raw")
|
||||
os.makedirs(base, exist_ok=True)
|
||||
except Exception:
|
||||
debug_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ocr_debug")
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return base
|
||||
|
||||
|
||||
def _get_ocr_processed_dir() -> str:
|
||||
"""OCR 处理后图片(JPG)保存目录:源码目录下的 img/processed。"""
|
||||
try:
|
||||
os.makedirs(debug_dir, exist_ok=True)
|
||||
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img", "processed")
|
||||
os.makedirs(base, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
return debug_dir
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return base
|
||||
|
||||
|
||||
def _get_easyocr_crash_log() -> str:
|
||||
"""返回 easyocr_worker 崩溃日志路径,必须与 easyocr_worker.py 中 CRASH_LOG 保持一致。
|
||||
|
||||
放在 %LOCALAPPDATA%/windowmsgbot/easyocr_crash.log,避免在源码目录写文件(防 360)。
|
||||
放在源码目录根(easyocr_crash.log),统一管理、便于父进程读取(不写 %LOCALAPPDATA%)。
|
||||
"""
|
||||
try:
|
||||
local_appdata = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
|
||||
if local_appdata:
|
||||
base = os.path.join(local_appdata, "windowmsgbot")
|
||||
else:
|
||||
base = os.path.join(os.path.expanduser("~"), "windowmsgbot")
|
||||
except Exception:
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
except Exception:
|
||||
base = "."
|
||||
return os.path.join(base, "easyocr_crash.log")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user