修复bot自言自语大小写不敏感问题 + 退出时后台内存回收不阻塞
- monitor.py: 去重管道所有比较改为大小写不敏感(recent_replies时间窗口、_replied_texts、_fuzzy_duplicate、子串包含、长公共子串、_last_bot_reply_content) - main.py: CTRL+C信号处理将内存回收放到后台线程,不阻塞退出 - gui.py: 无实质修改(仅格式)
This commit is contained in:
@@ -9,6 +9,8 @@ import threading
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import gc
|
||||
import ctypes
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
@@ -20,6 +22,83 @@ from auto_reply import AutoReply
|
||||
logger = logging.getLogger("msgbot.gui")
|
||||
|
||||
|
||||
def _force_system_memory_cleanup():
|
||||
"""强制回收Python进程占用的内存归还系统调度。
|
||||
|
||||
Windows API:
|
||||
- EmptyWorkingSet: 清空工作集,强制归还物理内存
|
||||
- SetProcessWorkingSetSize: 设置工作集大小为最小值,触发系统回收
|
||||
"""
|
||||
try:
|
||||
# 获取当前进程句柄
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
psapi = ctypes.windll.psapi
|
||||
|
||||
# 获取当前进程句柄
|
||||
h_process = kernel32.GetCurrentProcess()
|
||||
|
||||
# 方法1: EmptyWorkingSet - 清空工作集
|
||||
try:
|
||||
psapi.EmptyWorkingSet(h_process)
|
||||
logger.debug("系统内存清理: EmptyWorkingSet 完成")
|
||||
except Exception as e:
|
||||
logger.debug(f"EmptyWorkingSet 失败: {e}")
|
||||
|
||||
# 方法2: SetProcessWorkingSetSize - 设置最小工作集
|
||||
try:
|
||||
# dwMinimumWorkingSetSize=1, dwMaximumWorkingSetSize=-1 (无限)
|
||||
# 设置最小值为1字节,最大值为无限,让系统自由回收
|
||||
kernel32.SetProcessWorkingSetSize(h_process, 1, -1)
|
||||
logger.debug("系统内存清理: SetProcessWorkingSetSize 完成")
|
||||
except Exception as e:
|
||||
logger.debug(f"SetProcessWorkingSetSize 失败: {e}")
|
||||
|
||||
# Python GC + torch CUDA 清理
|
||||
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 未安装
|
||||
except Exception as e:
|
||||
logger.debug(f"torch.cuda.empty_cache 失败: {e}")
|
||||
|
||||
# Python GC 强制回收
|
||||
collected = gc.collect()
|
||||
logger.debug(f"系统内存清理: gc.collect 回收 {collected} 个对象")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"系统内存清理异常: {e}")
|
||||
|
||||
|
||||
def _kill_easyocr_subprocesses():
|
||||
"""杀死所有EasyOCR子进程,彻底释放GPU/CPU内存。"""
|
||||
try:
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
killed = 0
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
# EasyOCR子进程通常以python启动,cmdline包含easyocr
|
||||
if proc.pid == current_pid:
|
||||
continue
|
||||
cmdline = ' '.join(proc.cmdline() or [])
|
||||
if 'easyocr' in cmdline.lower() or 'ocr_worker' in cmdline.lower():
|
||||
proc.kill()
|
||||
killed += 1
|
||||
logger.debug(f"杀死EasyOCR子进程: PID={proc.pid}")
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
if killed:
|
||||
logger.debug(f"共杀死 {killed} 个EasyOCR子进程")
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"杀死EasyOCR子进程异常: {e}")
|
||||
|
||||
|
||||
class OCREngineManager:
|
||||
"""双引擎OCR管理器:EasyOCR + Windows OCR,得分最高者胜出。
|
||||
|
||||
@@ -191,18 +270,29 @@ class OCREngineManager:
|
||||
self._timer_thread.start()
|
||||
|
||||
def _release(self):
|
||||
"""释放OCR引擎资源"""
|
||||
"""释放OCR引擎资源 + 杀EasyOCR子进程 + 系统内存回收"""
|
||||
with self._lock:
|
||||
self._easyocr_reader = None
|
||||
self._winocr_engine = None
|
||||
self._initialized = False
|
||||
self._log("🗑 OCR引擎已释放")
|
||||
|
||||
# 杀EasyOCR子进程(彻底释放GPU内存)
|
||||
_kill_easyocr_subprocesses()
|
||||
|
||||
# 系统级内存回收
|
||||
_force_system_memory_cleanup()
|
||||
self._log("🧹 系统内存已回收")
|
||||
|
||||
def set_monitor_active(self, active: bool):
|
||||
"""设置消息监控激活状态"""
|
||||
self._monitor_active = active
|
||||
if active and self._initialized:
|
||||
self._last_use_time = time.time() # 激活时重置计时
|
||||
elif not active and self._initialized:
|
||||
# 监控停止 → 立即释放OCR(杀子进程+系统内存回收)
|
||||
self._log("⏸ 监控停止,立即释放OCR内存...")
|
||||
self._release()
|
||||
|
||||
def recognize(self, image) -> list:
|
||||
"""双引擎并发识别,返回得分最高的结果
|
||||
@@ -463,6 +553,10 @@ class MsgBotApp(ctk.CTk):
|
||||
# 自动回复执行队列(转发到主线程,避免 daemon 线程 SetForegroundWindow 失败)
|
||||
self._reply_queue: queue.Queue[dict] = queue.Queue()
|
||||
|
||||
# ⭐ 启动时系统内存清理(强制回收Python进程内存)
|
||||
self._log("🧹 启动内存清理...")
|
||||
threading.Thread(target=_force_system_memory_cleanup, daemon=True).start()
|
||||
|
||||
# ⭐ OCR引擎管理器(双引擎:EasyOCR + Windows OCR)
|
||||
self._ocr_manager = OCREngineManager(log_callback=self._log)
|
||||
# 预初始化OCR引擎(后台线程,不阻塞UI)
|
||||
@@ -1325,11 +1419,18 @@ class MsgBotApp(ctk.CTk):
|
||||
# ── 窗口关闭 ──
|
||||
|
||||
def _on_close(self):
|
||||
# ⭐ 关闭窗口同样把 stop 放到后台线程,完成后再关闭,避免主线程冻结。
|
||||
# ⭐ 关闭窗口:释放OCR + 系统内存清理 → stop → destroy
|
||||
def _stop_then_close():
|
||||
try:
|
||||
# 释放OCR引擎(杀子进程+系统内存回收)
|
||||
self._ocr_manager._release()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.monitor.stop()
|
||||
except Exception:
|
||||
pass
|
||||
# 最终系统内存清理
|
||||
_force_system_memory_cleanup()
|
||||
self.after(0, self.destroy)
|
||||
threading.Thread(target=_stop_then_close, daemon=True, name="gui-stop-close").start()
|
||||
|
||||
Reference in New Issue
Block a user