fix: QQ截图cap_left右移至25%避开消息列表时间戳; 修复未来时间戳误判为新鲜(14:40不能匹配16:00); 修复跨午夜反向边界+1day误用; 修复_is_markdown_document日志插入导致长文本检测死代码; 四层过滤器加日志
This commit is contained in:
+56
-9
@@ -1292,7 +1292,7 @@ class MessageMonitor:
|
||||
# 计数 + 主动重启:EasyOCR GPU 内存泄漏累积 → OOM → 冻结
|
||||
self._easyocr_call_count += 1
|
||||
if self._easyocr_call_count >= self._easyocr_max_calls:
|
||||
logger.debug(f"[ocr] EasyOCR 已达 {self._easyocr_max_calls} 次调用,重启子进程清 GPU 缓存")
|
||||
logger.debug(f"[ocr] EasyOCR 正常资源释放:{self._easyocr_max_calls}次调用完成,重启子进程清GPU缓存(非错误,不计入失败计数器)")
|
||||
try:
|
||||
if self._easyocr_proc and self._easyocr_proc.poll() is None:
|
||||
self._easyocr_proc.kill()
|
||||
@@ -1581,8 +1581,8 @@ class MessageMonitor:
|
||||
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_END, 0)
|
||||
time.sleep(0.3)
|
||||
# TIM/QQ 窗口布局: ~5-30% 左栏, ~30-50% 会话列表(含文档预览), ~50-100% 消息面板
|
||||
# cap_left=15% 避开会话列表(用户反馈10%截到会话列表)
|
||||
cap_left = int(left + win_w * 0.15)
|
||||
# cap_left=25% 继续右移(用户反馈20%仍会捕捉到消息列表的时间戳,干扰消息判定)
|
||||
cap_left = int(left + win_w * 0.25)
|
||||
cap_top = top + max(80, int(win_h * 0.06)) # 跳过标题栏+群名头栏
|
||||
cap_right = int(left + win_w * 0.78) # 跳过右栏群成员列表(~80%+) 和滚动条
|
||||
cap_bottom = bottom - max(100, int(win_h * 0.10)) # 输入框上方
|
||||
@@ -1942,6 +1942,7 @@ class MessageMonitor:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
expanded.append(msg)
|
||||
buf = ''
|
||||
last_ts = part
|
||||
@@ -1977,6 +1978,7 @@ class MessageMonitor:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
# EasyOCR 常把横向相邻的两条消息(用户消息+群昵称/UI文字)
|
||||
# 检测为同一行,导致"兑换码导入错误时如何处置?游戏专用小筑憩-草一"
|
||||
# 这种合并文本。尝试在中文句尾标点处拆分为多条。
|
||||
@@ -2974,7 +2976,9 @@ class MessageMonitor:
|
||||
|
||||
def _is_reply_fragment(self, text: str) -> bool:
|
||||
"""检查文本是否为 bot 回复内容的碎片(OCR 截取的 md 残片/跨行碎片)。"""
|
||||
logger.debug(f"[ocr] 过滤器4: _is_reply_fragment 检查: {text[:80]!r}")
|
||||
if not self._replied_texts or len(text) < 6:
|
||||
logger.debug(f"[ocr] 过滤器4结果: 无已回复文本或文本太短({len(text)}<6),返回False")
|
||||
return False
|
||||
import difflib
|
||||
# 原始文本匹配(含字母数字)
|
||||
@@ -2983,6 +2987,7 @@ class MessageMonitor:
|
||||
continue
|
||||
sm = difflib.SequenceMatcher(None, text, r)
|
||||
if sm.find_longest_match(0, len(text), 0, len(r)).size >= 6:
|
||||
logger.debug(f"[ocr] 过滤器4结果: 原始文本匹配成功,返回True")
|
||||
return True
|
||||
# CJK-only 降阈值
|
||||
cjk_t = ''.join(c for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
@@ -2993,7 +2998,9 @@ class MessageMonitor:
|
||||
continue
|
||||
sm = difflib.SequenceMatcher(None, cjk_t, r_cjk)
|
||||
if sm.find_longest_match(0, len(cjk_t), 0, len(r_cjk)).size >= 5:
|
||||
logger.debug(f"[ocr] 过滤器4结果: CJK文本匹配成功,返回True")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器4结果: 未匹配到已回复文本,返回False")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@@ -3024,27 +3031,34 @@ class MessageMonitor:
|
||||
有时间戳但全部过期 → 仅当最新时间戳不太老(≤600s)且文本含明确新鲜特征时放行,
|
||||
否则视为历史消息丢弃
|
||||
"""
|
||||
logger.debug(f"[ocr] === 过滤器1: _ocr_text_has_fresh_timestamp 开始检查 ===")
|
||||
logger.debug(f"[ocr] 输入文本: {text[:100]!r}")
|
||||
if not text:
|
||||
logger.debug(f"[ocr] 过滤器1结果: 空文本,返回False")
|
||||
return False
|
||||
# 标准化 EasyOCR 时间戳:16.47 → 16:47, 15;39 → 15:39, 16:03.41 → 16:03:41
|
||||
normalized = re.sub(r'(\d{1,2})[.;](\d{2})(?:[.;](\d{2}))?', r'\1:\2:\3', text)
|
||||
timestamps = re.findall(r'\d{1,2}:\d{1,2}(?::\d{1,2})?', normalized)
|
||||
logger.debug(f"[ocr] 提取到时间戳: {timestamps}")
|
||||
if not timestamps:
|
||||
# ⭐ 无可见时间戳:仅超短中文消息(如"好的""嗯")视为可能的新消息放行;
|
||||
# 较长文本无时间戳 → 无法验证新鲜性,可能是不含时间戳的历史/UI噪声
|
||||
# → 丢弃,防止旧消息因整批无时间戳而绕过 stage splitting 放行
|
||||
# 但如果文本包含噪声(HTML/图片路径等),直接丢弃
|
||||
logger.debug(f"[ocr] 无可见时间戳")
|
||||
if self._is_noise_text(text):
|
||||
logger.debug(f"[ocr] 无可见时间戳且为噪声文本,丢弃: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 无时间戳且为噪声文本,丢弃: {text[:80]!r}")
|
||||
return False
|
||||
if len(text.strip()) <= 4 and re.search(r'[\u4e00-\u9fff]', text):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 无时间戳但为超短中文消息,放行: {text[:80]!r}")
|
||||
return True
|
||||
logger.debug(f"[ocr] 无可见时间戳且非超短消息,丢弃: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 无时间戳且非超短消息,丢弃: {text[:80]!r}")
|
||||
return False
|
||||
# 有新鲜时间戳 → 确定有新消息
|
||||
for ts in timestamps:
|
||||
try:
|
||||
if not self._is_timestamp_old(ts, 300):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 发现新鲜时间戳{ts}(≤300s),放行")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
@@ -3058,18 +3072,25 @@ class MessageMonitor:
|
||||
except Exception:
|
||||
pass
|
||||
if not newest_fresh_enough:
|
||||
logger.debug(f"[ocr] 最新可见时间戳已过期≥600s,丢弃: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 最新可见时间戳已过期≥600s,丢弃: {text[:80]!r}")
|
||||
return False
|
||||
# 最新时间戳在 300-600s 之间,且文本含明确新鲜特征 → 放行给 stage splitting
|
||||
if self._looks_fresh_content(text):
|
||||
logger.debug(f"[ocr] 时间戳全过期但最新≤600s且含新鲜特征,放行: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期但最新≤600s且含新鲜特征,放行: {text[:80]!r}")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期且无新鲜特征,丢弃: {text[:80]!r}")
|
||||
return False
|
||||
|
||||
# 未来时间戳容忍偏差(秒):同日内时间戳比系统时间晚超过此值 = 未到来,直接丢弃。
|
||||
# 容忍小量偏差避免误杀:OCR/聊天服务器时钟与系统时钟存在秒级差异,刚到的消息
|
||||
# 时间戳可能比系统时间晚几秒(如 14:41:00 消息在 14:40:58 被捕获)。
|
||||
_TS_FUTURE_TOLERANCE_S = 15
|
||||
|
||||
@staticmethod
|
||||
def _is_timestamp_old(ts_str: str, max_age: int = 300) -> bool:
|
||||
"""检查 HH:MM 或 HH:MM:SS 时间戳是否超过 max_age 秒。
|
||||
处理跨午夜边界:如果 diff > 12h 说明是昨天的时间戳。
|
||||
未来时间戳硬拦截:同日内 diff < -容差 = 时间未到来(系统14:40不可能有16:00),丢弃。
|
||||
OCR 误读兜底:15:31→5:31(`1`笔画细被吞),若 h<12,尝试 h+10/h+12
|
||||
取 diff 最小者,若 < max_age 则视为 OCR 误读,放行。
|
||||
返回 True = 时间戳太老,应跳过。
|
||||
@@ -3095,10 +3116,16 @@ class MessageMonitor:
|
||||
if diff > 43200:
|
||||
parsed = parsed - timedelta(days=1)
|
||||
diff = (now - parsed).total_seconds()
|
||||
# 未来时间戳: diff < -12h 说明是明天(当前 23:55,时间戳 00:05)
|
||||
# 跨午夜反向: diff < -12h 说明是昨天晚些时候(当前 00:05,时间戳 23:55 → 昨天23:55)
|
||||
# ⚠ 必须 -1day:之前误用 +1day 得到 -47h 的负 diff → 返回"不旧"=新鲜 → 凌晨误判
|
||||
if diff < -43200:
|
||||
parsed = parsed + timedelta(days=1)
|
||||
parsed = parsed - timedelta(days=1)
|
||||
diff = (now - parsed).total_seconds()
|
||||
# 未来时间戳硬拦截(用户反馈:系统14:40 不能把 16:00 未到来的时间判为新鲜):
|
||||
# 跨午夜调整后仍为负 = 同日内未来时间,必为 OCR 误读/异常,直接丢弃
|
||||
if diff < -MessageMonitor._TS_FUTURE_TOLERANCE_S:
|
||||
logger.debug(f"[ts] 未来时间戳拦截: {ts} 比系统时间 {now.strftime('%H:%M:%S')} 晚 {-diff:.0f}s,未到来,丢弃")
|
||||
return True
|
||||
# OCR 误读兜底:h<12 且判定为老 → 尝试 h+10 / h+12(OCR 丢失前导1:15→5)
|
||||
if diff > max_age and h < 12:
|
||||
best_h, best_diff = h, diff
|
||||
@@ -3246,31 +3273,41 @@ class MessageMonitor:
|
||||
Bot 回复用 md 格式(# 标题、[链接](url)、列表等),用户日常对话不用。
|
||||
借此区分 bot 旧回复和历史堆积 vs 用户新消息。
|
||||
"""
|
||||
logger.debug(f"[ocr] 过滤器3: _is_markdown_document 检查: {text[:80]!r}")
|
||||
if not text:
|
||||
logger.debug(f"[ocr] 过滤器3结果: 空文本,返回False")
|
||||
return False
|
||||
# # 行首标题:短文本也检测(OCR 把 ##协议修 切成 5 字符片段)
|
||||
if re.match(r'^#{1,6}(?:\s|\[|[\u4e00-\u9fff])', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到Markdown标题语法,返回True")
|
||||
return True
|
||||
# 行首列表项:短文本也检测(- 玩家义务 / * 注意)
|
||||
# 必须放 len<10 之前,否则 "- 玩家应尊重他"(8c) 漏网
|
||||
if re.match(r'^[\-\*]\s', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到Markdown列表语法,返回True")
|
||||
return True
|
||||
# 全角括号退化 md 链接:短文本也检测
|
||||
if re.search(r'\](', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到全角括号退化链接,返回True")
|
||||
return True
|
||||
if re.search(r'\[.+?\](', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到全角括号退化链接模式2,返回True")
|
||||
return True
|
||||
# OCR 退化 md checkbox:短文本也检测([V] / [x])
|
||||
if re.search(r'\[\s*[VXvx√✗]\s*\]', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到OCR退化checkbox,返回True")
|
||||
return True
|
||||
# OCR 退化 md 链接缺括号:短文本也检测
|
||||
if re.search(r'\[.+?\]\(', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到OCR退化链接,返回True")
|
||||
return True
|
||||
# **加粗**语法:短文本也检测(bot md 回复标配,用户日常不用)
|
||||
if re.search(r'\*\*.+?\*\*', text):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到加粗语法,返回True")
|
||||
return True
|
||||
# <br> HTML 换行标记 — bot md 回复中用于换行,用户聊天不打
|
||||
if re.search(r'<\s*br\s*/?\s*>', text, re.IGNORECASE):
|
||||
logger.debug(f"[ocr] 过滤器3结果: 检测到HTML换行标记,返回True")
|
||||
return True
|
||||
# ###1. / ####数字标题 — 数字直接跟在 # 后(OCR 常见)
|
||||
if re.match(r'^#{1,6}\d', text):
|
||||
@@ -3292,6 +3329,7 @@ class MessageMonitor:
|
||||
if '**' in text:
|
||||
return True
|
||||
if len(text) < 10:
|
||||
logger.debug(f"[ocr] 过滤器3结果: 短文本(<10)无Markdown特征,返回False")
|
||||
return False
|
||||
# ##直接跟汉字(OCR 丢失 ##后空格时常见:##文档导航)
|
||||
if re.match(r'^#{1,6}[\u4e00-\u9fff]', text):
|
||||
@@ -3321,26 +3359,35 @@ class MessageMonitor:
|
||||
这些文本通常来自QQ消息中的引用/图片附属内容,或OCR识别错误。
|
||||
返回 True = 应当丢弃。
|
||||
"""
|
||||
logger.debug(f"[ocr] 过滤器2: _is_noise_text 检查: {text[:80]!r}")
|
||||
if not text:
|
||||
logger.debug(f"[ocr] 过滤器2结果: 空文本,返回False")
|
||||
return False
|
||||
# HTML 标签:<br>、<br/>、<p>、</p>、<div>等
|
||||
if re.search(r'<\s*(?:br|p|div|span|a|img)\s*/?\s*>', text, re.IGNORECASE):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到HTML标签,返回True")
|
||||
return True
|
||||
# 图片路径引用:、!0(Jimg/...)、=等
|
||||
if re.search(r'!\s*\[.*?\]\s*\(.*?\)', text):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到图片路径引用模式1,返回True")
|
||||
return True
|
||||
# 简化图片路径:!0(Jimg/...) 或 
|
||||
if re.search(r'!\s*\[.*?\]\s*\(', text):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到图片路径引用模式2,返回True")
|
||||
return True
|
||||
# 纯图片路径:./img/... 或 img/...
|
||||
if re.search(r'(?:^|\s)\.?(?:img|image|pic|photo)/\S+', text, re.IGNORECASE):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到纯图片路径,返回True")
|
||||
return True
|
||||
# OCR 噪声字符模式:!0(J、= 等
|
||||
if re.search(r'![\[\(]\S+[\]\)]', text):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到OCR噪声字符模式,返回True")
|
||||
return True
|
||||
# 编辑权限等UI文本残留(用户截图中出现)
|
||||
if re.search(r'编辑权限', text):
|
||||
logger.debug(f"[ocr] 过滤器2结果: 检测到UI文本残留,返回True")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器2结果: 未检测到噪声特征,返回False")
|
||||
return False
|
||||
|
||||
def _paste_and_send_safe(self, text: str, hwnd: int, msg_hash: int, platform: str) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user