QQ轮询加速+故障降级:EasyOCR超时8s/原图降级/截图存TEMP
This commit is contained in:
+67
-18
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user