feat: monitor→GUI日志回调,EasyOCR子进程初始化状态可在GUI日志中查看

- MessageMonitor 新增 _log_callback / set_log_callback / _gui_log
- _delayed_easyocr_start 启动时输出「🔧 EasyOCR 子进程初始化中...」
- _handshake_easyocr_subprocess 成功/超时/失败时输出对应状态
- _start_easyocr_proc_locked 启动失败时输出错误
- start() 中 _HAS_EASYOCR=False 或 broken 时输出警告
- gui.py 在 _log_queue 创建后调用 monitor.set_log_callback(self._log)
This commit is contained in:
youpaishidifu
2026-08-30 12:26:59 +08:00
parent d2a4b4c0ea
commit 96f07353ba
2 changed files with 66 additions and 50 deletions
+24 -42
View File
@@ -54,14 +54,17 @@ def _force_system_memory_cleanup():
logger.debug(f"SetProcessWorkingSetSize 失败: {e}")
# Python GC + torch CUDA 清理
# ⭐ Task #1630: 修复内存687M——不主动 import torch(EasyOCR 已子进程化)
# import torch 会加载 ~300MB DLL;主进程无 CUDA 状态,仅在 torch 已加载时才清理
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
logger.debug("系统内存清理: torch.cuda.empty_cache 完成")
except ImportError:
pass # torch 未安装
import sys as _sys
if 'torch' in _sys.modules:
_torch = _sys.modules['torch']
if _torch.cuda.is_available():
_torch.cuda.empty_cache()
_torch.cuda.synchronize()
logger.debug("系统内存清理: torch.cuda.empty_cache 完成")
# else: torch 未加载,跳过
except Exception as e:
logger.debug(f"torch.cuda.empty_cache 失败: {e}")
@@ -221,7 +224,13 @@ class OCREngineManager:
return False
def initialize(self):
"""预初始化两套OCR引擎(EasyOCR GPU/CPU根据内存自动选择)"""
"""预初始化OCR引擎(仅 Windows OCR)。
⭐ Task #1630: 修复内存687M——不在主进程中 import easyocr/torch!
EasyOCR 已完全子进程化(easyocr_worker.py),主进程 import easyocr 会连带
加载 torch DLL (~300MB),且 Python 无法卸载已加载的 DLL,导致内存永远不释放。
主进程仅使用 Windows OCR;EasyOCR 通过 monitor.py 的子进程隔离方案使用。
"""
if self._initialized:
return True
@@ -231,38 +240,8 @@ class OCREngineManager:
self._log("🔧 初始化OCR引擎...")
# 初始化EasyOCR(根据内存条件选择GPU/CPU)
# ⭐ Task #1601: GPU 探针已拦截 CUDA 异常,此处用 try/except 兜底
# access violation (native crash) 无法被 Python 捕获,
# 但探针通过后此概率极低;CPU 模式不受 native crash 影响。
use_gpu = False
try:
import easyocr
use_gpu = self._check_memory_for_gpu()
mode_str = "GPU" if use_gpu else "CPU"
self._log(f" ⏳ 初始化EasyOCR ({mode_str}模式)...")
try:
self._easyocr_reader = easyocr.Reader(['ch_sim', 'en'], gpu=use_gpu, verbose=False)
self._log(f" ✅ EasyOCR就绪({mode_str}模式)")
except Exception as e:
# GPU失败 → 降级CPU重试(注意:access violation 不可被 except 捕获)
if use_gpu:
self._log(f" ⚠ EasyOCR GPU初始化失败: {e},降级CPU重试...")
try:
self._easyocr_reader = easyocr.Reader(['ch_sim', 'en'], gpu=False, verbose=False)
self._log(" ✅ EasyOCR就绪(CPU模式,GPU降级)")
except Exception as e2:
self._log(f" ⚠ EasyOCR CPU也失败: {e2}")
self._easyocr_reader = None
else:
self._log(f" ⚠ EasyOCR初始化失败: {e}")
self._easyocr_reader = None
except ImportError:
self._log(" ⚠ EasyOCR未安装")
self._easyocr_reader = None
except Exception as e:
self._log(f" ⚠ EasyOCR异常: {e}")
self._easyocr_reader = None
# ⭐ EasyOCR 不再在主进程中初始化——子进程隔离方案已接管
self._easyocr_reader = None
# 初始化Windows OCR
try:
@@ -277,12 +256,12 @@ class OCREngineManager:
self._log(f" ⚠ Windows OCR初始化失败: {e}")
self._winocr_engine = None
if not self._easyocr_reader and not self._winocr_engine:
if not self._winocr_engine:
self._log("❌ 无可用OCR引擎")
return False
self._initialized = True
self._use_gpu = use_gpu
self._use_gpu = False
self._last_use_time = time.time()
self._start_timeout_timer()
self._log("✅ OCR引擎初始化完成")
@@ -602,6 +581,9 @@ class MsgBotApp(ctk.CTk):
# 自动回复执行队列(转发到主线程,避免 daemon 线程 SetForegroundWindow 失败)
self._reply_queue: queue.Queue[dict] = queue.Queue()
# ⭐ monitor→GUI 日志回调:EasyOCR 子进程初始化等关键状态转发到 GUI 日志
self.monitor.set_log_callback(self._log)
# ⭐ 启动时系统内存清理(强制回收Python进程内存)
self._log("🧹 启动内存清理...")
threading.Thread(target=_force_system_memory_cleanup, daemon=True).start()
+42 -8
View File
@@ -632,6 +632,8 @@ class MessageMonitor:
self._emergency_thread: Optional[threading.Thread] = None
self._poll_interval = 2.0
self._on_message = on_message
# ⭐ 日志回调:monitor→GUI 转发关键状态(如 EasyOCR 子进程初始化进度)
self._log_callback: Optional[Callable[[str], None]] = None
self._wechat_enabled = True
self._qq_enabled = False
@@ -817,6 +819,18 @@ class MessageMonitor:
def set_callback(self, cb: Callable[[str, str], None]):
self._on_message = cb
def set_log_callback(self, cb: Callable[[str], None]):
"""设置日志回调:monitor→GUI 转发关键状态消息。"""
self._log_callback = cb
def _gui_log(self, msg: str):
"""转发关键状态到 GUI 日志(静默失败,不影响主流程)。"""
if self._log_callback:
try:
self._log_callback(msg)
except Exception:
pass
def set_emergency_callback(self, cb: Callable[[], None]):
self._emergency_stop_callback = cb
@@ -952,8 +966,13 @@ class MessageMonitor:
def _delayed_easyocr_start():
time.sleep(1.0)
if self._running:
self._gui_log("🔧 EasyOCR 子进程初始化中...")
self._ensure_easyocr_subprocess()
threading.Thread(target=_delayed_easyocr_start, daemon=True, name="easyocr-delay").start()
elif not _HAS_EASYOCR:
self._gui_log("⚠ EasyOCR 不可用(导入失败),仅使用 Windows OCR")
elif self._easyocr_broken:
self._gui_log("⚠ EasyOCR 已标记损坏,跳过初始化")
logger.info("消息监控已启动")
@@ -1083,15 +1102,23 @@ class MessageMonitor:
except Exception as e:
logger.debug(f"[memory] 内存释放调用失败: {e}")
# 3) 归还 GPU 显存(若 torch/CUDA 可用)
# 3) 归还 GPU 显存(仅当 torch 已加载时)
# ⭐ Task #1630: 修复内存687M——主进程不做 torch CUDA,不 import torch!
# import torch 会加载 ~300MB DLL 到主进程,且 EasyOCR 已子进程化,主进程无 CUDA 状态。
# 仅在 torch 已被意外加载时才尝试清理;绝不主动 import torch。
try:
import torch
if torch.cuda.is_available():
torch.cuda.synchronize()
torch.cuda.empty_cache()
logger.debug("[memory] 已调用 torch.cuda.empty_cache 归还显存")
import sys
if 'torch' in sys.modules:
_torch = sys.modules['torch']
if _torch.cuda.is_available():
_torch.cuda.synchronize()
_torch.cuda.empty_cache()
logger.debug("[memory] 已调用 torch.cuda.empty_cache 归还显存")
else:
logger.debug("[memory] torch 已加载但 CUDA 不可用,跳过显存清理")
else:
logger.debug("[memory] torch 未加载,跳过显存清理(EasyOCR 子进程化,主进程无 CUDA 状态)")
except Exception:
# torch 未安装或当前为 CPU 推理,忽略
pass
# 再次垃圾回收,确保上述释放后的垃圾被最终回收
@@ -2468,6 +2495,7 @@ class MessageMonitor:
return True
except Exception as e:
logger.error(f"[ocr] 启动 EasyOCR 子进程失败: {e}")
self._gui_log(f"❌ EasyOCR 子进程启动失败: {e}")
self._easyocr_broken = True
self._easyocr_proc = None
return False
@@ -2493,6 +2521,7 @@ class MessageMonitor:
except _QueueEmpty:
logger.error("[ocr] EasyOCR 子进程初始化超时(60s),杀死进程"
+ ("(正常重启,不施加冷却)" if is_normal_restart else ",30s 后重试"))
self._gui_log("❌ EasyOCR 子进程初始化超时(60s)")
self._kill_easyocr_proc("初始化超时")
if not is_normal_restart:
self._easyocr_retry_until = time.time() + 30
@@ -2537,6 +2566,7 @@ class MessageMonitor:
payload = json.loads(line)
if payload.get("ready"):
logger.info("[ocr] EasyOCR 子进程就绪")
self._gui_log("✅ EasyOCR 子进程就绪")
self._easyocr_consecutive_startup_failures = 0 # 成功启动,重置启动失败计数
# ⭐ 截断崩溃日志:清除旧条目,防止旧记录混淆用户
_truncate_easyocr_crash_log()
@@ -6928,10 +6958,14 @@ class MessageMonitor:
except ValueError:
return None
diff = (now - parsed).total_seconds()
# ⭐ Task #1627: 修复时间戳年龄计算——负数diff表示时间戳在今天还没到(parsed在未来),
# 但实际时间戳可能是昨天的,所以往前减一天重新计算
# 原逻辑仅当 diff < -43200(12小时)才调整,导致负数diff(如-25200秒)被截断为0,
# 使得昨天的时间戳被误判为"年龄0秒"(最新),今天实际最新消息被忽略
if diff > 43200:
parsed = parsed - timedelta(days=1)
diff = (now - parsed).total_seconds()
if diff < -43200:
if diff < 0:
parsed = parsed - timedelta(days=1)
diff = (now - parsed).total_seconds()
return max(diff, 0)