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:
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user