新增截图OCR识别昵称功能
- GUI新增「从截图识别」按钮 - 截屏后用EasyOCR识别文字(GPU=False) - 提取QQ昵称模式:[头衔]用户名、@用户名、行首用户名 - 自动去重并入库config.json的qq_nicknames - Windows OCR作为后备方案
This commit is contained in:
@@ -261,7 +261,13 @@ class MsgBotApp(ctk.CTk):
|
||||
nick_btn_frame, text="刷新", width=56,
|
||||
command=self._refresh_nick_list
|
||||
)
|
||||
self._refresh_nick_btn.pack(side="left")
|
||||
self._refresh_nick_btn.pack(side="left", padx=(0, 4))
|
||||
|
||||
self._ocr_nick_btn = ctk.CTkButton(
|
||||
nick_btn_frame, text="📸 从截图识别", width=100,
|
||||
command=self._ocr_nicknames_from_screenshot
|
||||
)
|
||||
self._ocr_nick_btn.pack(side="left")
|
||||
|
||||
# 初始加载昵称列表
|
||||
self._refresh_nick_list()
|
||||
@@ -423,6 +429,180 @@ class MsgBotApp(ctk.CTk):
|
||||
except Exception as e:
|
||||
self._log(f"⚠ 删除失败: {e}")
|
||||
|
||||
def _ocr_nicknames_from_screenshot(self):
|
||||
"""截图OCR识别昵称:最小化窗口→截屏→OCR→提取昵称→入库"""
|
||||
self._log("📸 正在截屏,请确保QQ聊天窗口可见...")
|
||||
self._ocr_nick_btn.configure(state="disabled", text="截屏中...")
|
||||
|
||||
def _do_ocr():
|
||||
try:
|
||||
import time
|
||||
time.sleep(1.5) # 等待用户切换到QQ窗口
|
||||
|
||||
# 截取全屏
|
||||
screenshot = self._capture_screen()
|
||||
if screenshot is None:
|
||||
self._log_queue.put("⚠ 截屏失败")
|
||||
return
|
||||
|
||||
self._log_queue.put("🔍 OCR识别中...")
|
||||
|
||||
# OCR识别
|
||||
try:
|
||||
import easyocr
|
||||
reader = easyocr.Reader(['ch_sim', 'en'], gpu=False, verbose=False)
|
||||
results = reader.readtext(screenshot, detail=0)
|
||||
except Exception as e:
|
||||
self._log_queue.put(f"⚠ EasyOCR失败: {e},尝试Windows OCR...")
|
||||
results = self._win_ocr(screenshot)
|
||||
|
||||
if not results:
|
||||
self._log_queue.put("⚠ OCR未识别到任何文字")
|
||||
return
|
||||
|
||||
self._log_queue.put(f"📝 OCR识别到 {len(results)} 段文字")
|
||||
|
||||
# 从OCR结果中提取昵称
|
||||
nicks_found = self._extract_nicknames_from_ocr(results)
|
||||
if not nicks_found:
|
||||
self._log_queue.put("⚠ 未检测到QQ昵称模式([头衔]用户名 或 @用户名)")
|
||||
return
|
||||
|
||||
# 添加到配置
|
||||
self.cfg = load_config()
|
||||
existing = self.cfg.setdefault("qq_nicknames", [])
|
||||
added = []
|
||||
for n in nicks_found:
|
||||
if n not in existing:
|
||||
existing.append(n)
|
||||
added.append(n)
|
||||
|
||||
if added:
|
||||
save_config(self.cfg)
|
||||
self._refresh_nick_list()
|
||||
self._log_queue.put(f"✅ 新增 {len(added)} 个昵称: {', '.join(added)}")
|
||||
else:
|
||||
self._log_queue.put(f"ℹ 识别到的昵称已全部存在: {', '.join(nicks_found)}")
|
||||
|
||||
except Exception as e:
|
||||
self._log_queue.put(f"⚠ 截图OCR异常: {e}")
|
||||
finally:
|
||||
# 恢复按钮
|
||||
self.after(0, lambda: self._ocr_nick_btn.configure(
|
||||
state="normal", text="📸 从截图识别"))
|
||||
|
||||
threading.Thread(target=_do_ocr, daemon=True).start()
|
||||
|
||||
def _capture_screen(self):
|
||||
"""使用Windows API截取全屏,返回PIL Image"""
|
||||
try:
|
||||
import win32gui
|
||||
import win32ui
|
||||
import win32con
|
||||
from PIL import Image
|
||||
|
||||
# 获取屏幕尺寸
|
||||
screen_w = win32gui.GetSystemMetrics(win32con.SM_CXSCREEN)
|
||||
screen_h = win32gui.GetSystemMetrics(win32con.SM_CYSCREEN)
|
||||
|
||||
# 创建设备上下文
|
||||
hdesktop = win32gui.GetDesktopWindow()
|
||||
hdc = win32gui.GetDC(hdesktop)
|
||||
mfcDC = win32ui.CreateDCFromHandle(hdc)
|
||||
saveDC = mfcDC.CreateCompatibleDC()
|
||||
|
||||
# 创建位图
|
||||
saveBitMap = win32ui.CreateBitmap()
|
||||
saveBitMap.CreateCompatibleBitmap(mfcDC, screen_w, screen_h)
|
||||
saveDC.SelectObject(saveBitMap)
|
||||
|
||||
# 复制屏幕
|
||||
saveDC.BitBlt((0, 0), (screen_w, screen_h), mfcDC, (0, 0), win32con.SRCCOPY)
|
||||
|
||||
# 转为PIL Image
|
||||
bmpinfo = saveBitMap.GetInfo()
|
||||
bmpstr = saveBitMap.GetBitmapBits(True)
|
||||
img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
|
||||
bmpstr, 'raw', 'BGRX', 0, 1)
|
||||
|
||||
# 清理
|
||||
win32gui.DeleteObject(saveBitMap.GetHandle())
|
||||
saveDC.DeleteDC()
|
||||
mfcDC.DeleteDC()
|
||||
win32gui.ReleaseDC(hdesktop, hdc)
|
||||
|
||||
return img
|
||||
except Exception as e:
|
||||
logger.error(f"截屏失败: {e}")
|
||||
return None
|
||||
|
||||
def _win_ocr(self, image):
|
||||
"""Windows OCR后备方案(无需GPU)"""
|
||||
try:
|
||||
import io
|
||||
from PIL import Image
|
||||
# 保存为临时文件
|
||||
tmp_path = os.path.join(os.environ.get('TEMP', '.'), '_ocr_temp.png')
|
||||
image.save(tmp_path)
|
||||
|
||||
# 使用Windows OCR(需要winrt)
|
||||
from winrt.windows.media.ocr import OcrEngine
|
||||
from winrt.windows.graphics.imaging import BitmapDecoder
|
||||
from winrt.windows.storage import StorageFile
|
||||
import asyncio
|
||||
|
||||
async def _do_ocr():
|
||||
file = await StorageFile.get_file_from_path_async(tmp_path)
|
||||
stream = await file.open_read_async()
|
||||
decoder = await BitmapDecoder.create_async(stream)
|
||||
bitmap = await decoder.get_software_bitmap_async()
|
||||
engine = OcrEngine.try_create_from_user_profile_languages()
|
||||
result = await engine.recognize_async(bitmap)
|
||||
return result.text
|
||||
|
||||
text = asyncio.run(_do_ocr())
|
||||
# 按行分割
|
||||
return [line.strip() for line in text.split('\n') if line.strip()]
|
||||
except Exception as e:
|
||||
logger.error(f"Windows OCR失败: {e}")
|
||||
return []
|
||||
|
||||
def _extract_nicknames_from_ocr(self, ocr_results):
|
||||
"""从OCR识别结果中提取QQ昵称模式"""
|
||||
nicks = set()
|
||||
# 合并所有识别文本
|
||||
full_text = '\n'.join(ocr_results) if isinstance(ocr_results, list) else str(ocr_results)
|
||||
|
||||
# 模式1: [头衔]用户名 或 【头衔】用户名
|
||||
for m in re.finditer(r'[\[【]([^\]】]+)[】\]]\s*([A-Za-z0-9_\u4e00-\u9fff]{2,30})', full_text):
|
||||
nick = m.group(2).strip()
|
||||
if len(nick) >= 2:
|
||||
nicks.add(nick)
|
||||
|
||||
# 模式2: @用户名
|
||||
for m in re.finditer(r'@([A-Za-z0-9_\u4e00-\u9fff]{2,30})', full_text):
|
||||
nick = m.group(1).strip()
|
||||
if len(nick) >= 2:
|
||||
nicks.add(nick)
|
||||
|
||||
# 模式3: 行首的纯中文/英文用户名(2-15字符),后跟消息内容
|
||||
# 这个模式比较宽松,需要人工确认
|
||||
for line in ocr_results if isinstance(ocr_results, list) else full_text.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# 行首用户名:2-15字符的中英文,后跟逗号/冒号/空格+内容
|
||||
m = re.match(r'^([A-Za-z0-9_\u4e00-\u9fff]{2,15})\s*[,,::]\s*.+', line)
|
||||
if m:
|
||||
nick = m.group(1).strip()
|
||||
# 过滤常见非昵称词
|
||||
skip_words = {'你好', '请问', '怎么', '什么', '为什么', '多少', '在哪', '下载',
|
||||
'安装', '配置', '设置', '使用', '更新', '版本', '错误', '问题'}
|
||||
if nick not in skip_words:
|
||||
nicks.add(nick)
|
||||
|
||||
return list(nicks)
|
||||
|
||||
# ── 消息处理 ──
|
||||
|
||||
def _on_new_message(self, platform: str, msg: str):
|
||||
|
||||
Reference in New Issue
Block a user