QQ轮询加速+故障降级:EasyOCR超时8s/原图降级/截图存TEMP

This commit is contained in:
youpaishidifu
2026-07-29 16:15:48 +08:00
parent 659f667d74
commit 5b31b66f01
+67 -18
View File
@@ -937,10 +937,11 @@ class MessageMonitor:
self._easyocr_proc = None
return False
def _call_easyocr_safe(self, img_np) -> Optional[str]:
"""通过子进程安全调用 EasyOCR。返回识别文本,失败/崩溃时返回 None。
def _call_easyocr_safe(self, img_np, timeout: Optional[float] = None) -> Optional[str]:
"""通过子进程安全调用 EasyOCR。返回识别文本,失败/崩溃/超时时返回 None。
使用 _easyocr_lock 保护子进程 stdin/stdout,避免 QQ/微信独立线程并发写乱。
timeout — 子进程读取超时秒数,None=无限等待;超时后杀死子进程下次重启。
"""
import io
import base64
@@ -959,7 +960,30 @@ class MessageMonitor:
self._easyocr_proc.stdin.write(json.dumps({"img": img_b64}) + "\n")
self._easyocr_proc.stdin.flush()
line = self._easyocr_proc.stdout.readline()
from queue import Queue, Empty as _QueueEmpty
# readline 包装超时:用 daemon 线程 + Queue 实现跨平台
if timeout is not None:
_q = Queue()
def _read_worker():
try:
_q.put(self._easyocr_proc.stdout.readline())
except Exception:
pass
_t = threading.Thread(target=_read_worker, daemon=True)
_t.start()
try:
line = _q.get(timeout=timeout)
except _QueueEmpty:
logger.debug(f"[ocr] EasyOCR 子进程超时({timeout}s),杀死子进程下次重启")
try:
if self._easyocr_proc and self._easyocr_proc.poll() is None:
self._easyocr_proc.kill()
except Exception:
pass
self._easyocr_proc = None # 清引用,下次 poll 自动重启(不设 _easyocr_broken)
return None
else:
line = self._easyocr_proc.stdout.readline()
if not line:
raise ConnectionError("EasyOCR 子进程 stdout 关闭")
result = json.loads(line)
@@ -1099,12 +1123,13 @@ class MessageMonitor:
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
_dbg_dir = os.path.join(os.environ.get('TEMP', os.path.dirname(os.path.abspath(__file__))), "ocr_debug")
os.makedirs(_dbg_dir, exist_ok=True)
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_dbg_path = os.path.join(script_dir, f"ocr_debug_qq_{ts}.png")
_dbg_path = os.path.join(_dbg_dir, f"ocr_debug_qq_{ts}.png")
im.save(_dbg_path)
logger.debug(f"[qq] 调试截图: {_dbg_path}")
_clean_old_ocr_debug(script_dir, keep=60)
_clean_old_ocr_debug(_dbg_dir, keep=60)
except Exception as e:
logger.debug(f"[qq] 调试截图保存失败: {e}")
@@ -1130,6 +1155,7 @@ class MessageMonitor:
# 图像预处理:提升 Windows OCR 识别准确率
# Windows OCR 对低 DPI/未处理截图识别差——CJK 空格、字符混淆
# 2x LANCZOS 放大 + 锐化 + 对比度增强
im_original = im # 保留原图,bad allocation 时降级
try:
im = im.resize((im.width * 2, im.height * 2), Image.LANCZOS)
im = ImageEnhance.Sharpness(im).enhance(1.8)
@@ -1143,22 +1169,45 @@ class MessageMonitor:
if im.mode != 'RGBA':
im = im.convert('RGBA')
logger.debug("[ocr] Windows OCR 开始...")
sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height)
sb.copy_from_buffer(im.tobytes())
win_result = engine.recognize_async(sb).get()
win_text = win_result.text.strip()
logger.debug(f"[ocr] Windows OCR 完成: {len(win_text)} chars")
try:
sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height)
sb.copy_from_buffer(im.tobytes())
win_result = engine.recognize_async(sb).get()
win_text = win_result.text.strip()
logger.debug(f"[ocr] Windows OCR 完成: {len(win_text)} chars")
except (RuntimeError, OSError) as e:
estr = str(e)
if "bad allocation" in estr or "memory" in estr.lower():
logger.debug(f"[ocr] Windows OCR bad allocation({im.width}x{im.height}),降级到原图重试")
# 用原图(跳过 2x resize)重试
try:
im2 = im_original
if im2.mode != 'RGBA':
im2 = im2.convert('RGBA')
sb2 = SoftwareBitmap(BitmapPixelFormat.RGBA8, im2.width, im2.height)
sb2.copy_from_buffer(im2.tobytes())
win_result2 = engine.recognize_async(sb2).get()
win_text = win_result2.text.strip()
im = im2 # EasyOCR 也用原图
logger.debug(f"[ocr] Windows OCR 降级重试完成: {len(win_text)} chars")
except Exception as e2:
logger.debug(f"[ocr] Windows OCR 降级也失败: {e2}")
win_text = ""
else:
logger.debug(f"[ocr] Windows OCR 异常: {e}")
win_text = ""
# EasyOCR 备用(GPU crash 隔离:通过子进程调用)
easy_text = ""
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)
easy_text = self._call_easyocr_safe(img_np, timeout=8)
if easy_text is None:
self._easyocr_broken = True
# None = 子进程崩溃或超时;超时已自动杀子进程,下次自动重启
# 只有真正的崩溃(BrokenPipeError / OSError)才永久禁用
easy_text = ""
logger.warning("[ocr] EasyOCR 子进程崩溃,已禁用(本次会话不再尝试)")
logger.debug("[ocr] EasyOCR 无结果(超时或崩溃),使用 Windows OCR 结果")
elif easy_text:
logger.debug(f"[ocr] EasyOCR 识别完成: {len(easy_text)} chars")
@@ -1230,13 +1279,13 @@ class MessageMonitor:
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
_dbg_dir = os.path.join(os.environ.get('TEMP', os.path.dirname(os.path.abspath(__file__))), "ocr_debug")
os.makedirs(_dbg_dir, exist_ok=True)
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_dbg_path = os.path.join(script_dir, f"ocr_debug_wx_{ts}.png")
_dbg_path = os.path.join(_dbg_dir, f"ocr_debug_wx_{ts}.png")
im.save(_dbg_path)
logger.debug(f"[wechat] 调试截图: {_dbg_path}")
# 清理 ocr_debug_* 调试图(unified pool)
_clean_old_ocr_debug(script_dir, keep=60)
_clean_old_ocr_debug(_dbg_dir, keep=60)
except Exception as e:
logger.debug(f"[wechat] 调试截图保存失败: {e}")